Compare commits
23 Commits
09285d0a46
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1290456378 | |||
| 3ddcf1d238 | |||
| 07258420af | |||
| 17b16ddd9f | |||
| 72c0feb1f3 | |||
| ab1d46e0e3 | |||
| 1635b0c10a | |||
| 560a6ae20b | |||
| da9992e060 | |||
| cf55f3c9f9 | |||
| 44c440222a | |||
| 9fd39bd7d6 | |||
| 387241f971 | |||
| c222434f3c | |||
| 9deef06169 | |||
| e0592212e1 | |||
| 0c95c7d120 | |||
| ef19221b62 | |||
| 2d44647f94 | |||
| b3add6f0c2 | |||
| 4994d4a8ef | |||
| c06701673a | |||
| 11f6efad09 |
@@ -3,6 +3,7 @@ APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://jouwdriedee.nl
|
||||
APP_TIMEZONE=Europe/Amsterdam
|
||||
|
||||
APP_LOCALE=nl
|
||||
APP_FALLBACK_LOCALE=nl
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
# Zorg voor de juiste opslagpermissies en database bestand
|
||||
RUN mkdir -p /app/storage /app/bootstrap/cache /app/database \
|
||||
&& touch /app/database/database.sqlite \
|
||||
RUN mkdir -p /app/storage /app/bootstrap/cache /app/database /app/storage/logs \
|
||||
&& touch /app/database/database.sqlite /app/storage/logs/cron.log \
|
||||
&& chown -R www-data:www-data /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
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Communication;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
@@ -25,7 +27,16 @@ class AccountController extends Controller
|
||||
->latest()
|
||||
->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_copyright',
|
||||
'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
|
||||
@@ -92,6 +105,10 @@ class ContentController extends Controller
|
||||
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) {
|
||||
if ($request->has($field)) {
|
||||
$value = $request->input($field);
|
||||
@@ -101,8 +118,10 @@ class ContentController extends Controller
|
||||
|
||||
Setting::flushCache();
|
||||
|
||||
return redirect()->route('admin.content.index')
|
||||
->with('success', 'Layout en teksten zijn direct opgeslagen en live bijgewerkt in de webshop!');
|
||||
$tab = $request->input('active_tab');
|
||||
$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'];
|
||||
$defaultMailer = config('mail.default');
|
||||
$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 {
|
||||
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).' <'.e($fromAddress).'></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 & 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)
|
||||
->subject('Test E-mail vanaf Jouwdriedee.nl');
|
||||
->from($fromAddress, $fromName)
|
||||
->subject('Test E-mail vanaf Jouwdriedee.nl')
|
||||
->html($htmlContent)
|
||||
->text($plainContent);
|
||||
});
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('success', "Test e-mail succesvol verzonden via driver '{$defaultMailer}' naar {$recipient}! Controleer je inbox en eventueel je spambox.");
|
||||
return redirect()->route('admin.content.index', ['tab' => 'email'])
|
||||
->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) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MakerWorldController extends Controller
|
||||
@@ -31,6 +32,22 @@ class MakerWorldController extends Controller
|
||||
$apiCollections = [];
|
||||
|
||||
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) {
|
||||
$apiCollections = $this->makerWorld->getFavoritesCollections();
|
||||
}
|
||||
@@ -66,8 +83,21 @@ class MakerWorldController extends Controller
|
||||
*/
|
||||
public function staging(Request $request): View
|
||||
{
|
||||
$query = Product::with(['category', 'makerworldCollection'])
|
||||
->where('status', 'draft');
|
||||
$tab = $request->query('tab', 'draft');
|
||||
|
||||
$query = Product::with(['category', 'makerworldCollection']);
|
||||
|
||||
if ($tab === 'draft') {
|
||||
$query->where('status', 'draft');
|
||||
} elseif ($tab === 'published') {
|
||||
$query->whereNotNull('makerworld_id')
|
||||
->where(function ($q) {
|
||||
$q->where('status', 'published')
|
||||
->orWhereNull('status');
|
||||
});
|
||||
} elseif ($tab === 'all') {
|
||||
$query->whereNotNull('makerworld_id');
|
||||
}
|
||||
|
||||
if ($request->filled('collection_id')) {
|
||||
$query->where('makerworld_collection_id', $request->input('collection_id'));
|
||||
@@ -85,9 +115,26 @@ class MakerWorldController extends Controller
|
||||
$stagedProducts = $query->latest()->paginate(24)->withQueryString();
|
||||
$categories = Category::orderBy('name')->get();
|
||||
$collections = MakerWorldCollection::all();
|
||||
$totalStaged = Product::where('status', 'draft')->count();
|
||||
|
||||
return view('admin.makerworld.staging', compact('stagedProducts', 'categories', 'collections', 'totalStaged'));
|
||||
$countDraft = Product::where('status', 'draft')->count();
|
||||
$countPublished = Product::whereNotNull('makerworld_id')
|
||||
->where(function ($q) {
|
||||
$q->where('status', 'published')
|
||||
->orWhereNull('status');
|
||||
})->count();
|
||||
$countAll = Product::whereNotNull('makerworld_id')->count();
|
||||
$totalStaged = $countDraft;
|
||||
|
||||
return view('admin.makerworld.staging', compact(
|
||||
'stagedProducts',
|
||||
'categories',
|
||||
'collections',
|
||||
'tab',
|
||||
'countDraft',
|
||||
'countPublished',
|
||||
'countAll',
|
||||
'totalStaged'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,8 +185,26 @@ class MakerWorldController extends Controller
|
||||
->with('success', "{$addedCount} collecties van @{$userHandle} succesvol gekoppeld en gesynchroniseerd!");
|
||||
}
|
||||
|
||||
// Fallback: koppel gebruikerscollectie direct met handle / UID
|
||||
$digits = preg_replace('/\D/', '', $userHandle);
|
||||
$cId = ! empty($digits) ? $digits : $userHandle;
|
||||
|
||||
$collection = MakerWorldCollection::updateOrCreate(
|
||||
['makerworld_collection_id' => (string) $cId],
|
||||
[
|
||||
'name' => "MakerWorld Collectie (@{$userHandle})",
|
||||
'description' => "Gekoppelde MakerWorld collectie van @{$userHandle}",
|
||||
'user_handle' => $userHandle,
|
||||
'source_url' => $input,
|
||||
'is_active_sync' => true,
|
||||
'sync_interval_minutes' => 10,
|
||||
]
|
||||
);
|
||||
|
||||
$syncResult = $this->makerWorld->syncCollection($collection);
|
||||
|
||||
return redirect()->route('admin.makerworld.index', ['tab' => 'collections'])
|
||||
->with('warning', "Gebruikersprofiel @{$userHandle} herkend. Voer a.u.b. een specifieke collectie-link of ID in (bijv. https://makerworld.com/nl/collections/123456) of log in bij MakerWorld.");
|
||||
->with('success', "Collectie van @{$userHandle} succesvol gekoppeld! Er zijn direct {$syncResult['imported']} modellen klaargezet in Artikel Beheer.");
|
||||
}
|
||||
|
||||
if (! $collectionId) {
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use App\Services\PriceCalculatorService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -17,13 +19,26 @@ class ProductController extends Controller
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Product::with('category')->latest();
|
||||
$tab = $request->query('tab', 'all');
|
||||
$query = Product::with(['category', 'makerworldCollection'])->latest();
|
||||
|
||||
if ($tab === 'published') {
|
||||
$query->where(function ($q) {
|
||||
$q->where('status', 'published')
|
||||
->orWhereNull('status');
|
||||
});
|
||||
} elseif ($tab === 'draft') {
|
||||
$query->where('status', 'draft');
|
||||
} elseif ($tab === 'makerworld') {
|
||||
$query->whereNotNull('makerworld_id');
|
||||
}
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = $request->input('q');
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->where('name', 'like', "%{$q}%")
|
||||
->orWhere('description', 'like', "%{$q}%");
|
||||
->orWhere('description', 'like', "%{$q}%")
|
||||
->orWhere('makerworld_id', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,10 +46,23 @@ class ProductController extends Controller
|
||||
$query->where('category_id', $request->input('category_id'));
|
||||
}
|
||||
|
||||
$totalAll = Product::count();
|
||||
$totalPublished = Product::where('status', 'published')->orWhereNull('status')->count();
|
||||
$totalDraft = Product::where('status', 'draft')->count();
|
||||
$totalMakerWorld = Product::whereNotNull('makerworld_id')->count();
|
||||
|
||||
$products = $query->paginate(20)->withQueryString();
|
||||
$categories = Category::all();
|
||||
|
||||
return view('admin.products.index', compact('products', 'categories'));
|
||||
return view('admin.products.index', compact(
|
||||
'products',
|
||||
'categories',
|
||||
'tab',
|
||||
'totalAll',
|
||||
'totalPublished',
|
||||
'totalDraft',
|
||||
'totalMakerWorld'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +86,9 @@ class ProductController extends Controller
|
||||
'base_price' => 'required|numeric|min:0.01',
|
||||
'short_description' => 'nullable|string|max:500',
|
||||
'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',
|
||||
'weight_grams' => 'nullable|integer|min:0',
|
||||
'print_time_hours' => 'nullable|numeric|min:0',
|
||||
@@ -69,6 +99,7 @@ class ProductController extends Controller
|
||||
$validated['slug'] = Str::slug($validated['name']);
|
||||
$validated['is_featured'] = $request->boolean('is_featured');
|
||||
$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_colors'] = [
|
||||
'Zon Geel (Pikachu)', 'Vurig Rood (Charizard)', 'Hemelsblauw (Squirtle)',
|
||||
@@ -103,7 +134,9 @@ class ProductController extends Controller
|
||||
'base_price' => 'required|numeric|min:0.01',
|
||||
'short_description' => 'nullable|string|max:500',
|
||||
'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',
|
||||
'weight_grams' => 'nullable|integer|min:0',
|
||||
'print_time_hours' => 'nullable|numeric|min:0',
|
||||
@@ -113,6 +146,7 @@ class ProductController extends Controller
|
||||
|
||||
$validated['is_featured'] = $request->boolean('is_featured');
|
||||
$validated['is_active'] = $request->boolean('is_active');
|
||||
$validated['gallery_images'] = $this->cleanGalleryImages($request->input('gallery_images', []), $validated['image_url'] ?? null);
|
||||
|
||||
$product->update($validated);
|
||||
|
||||
@@ -120,6 +154,62 @@ class ProductController extends Controller
|
||||
->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.
|
||||
*/
|
||||
|
||||
@@ -62,6 +62,7 @@ class ForgotPasswordController extends Controller
|
||||
try {
|
||||
Mail::to($user->email)->send(new PasswordResetMail($user, $resetUrl));
|
||||
} catch (Throwable $e) {
|
||||
Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage());
|
||||
Log::error("Fout bij verzenden van wachtwoordherstel e-mail naar {$user->email}: ".$e->getMessage());
|
||||
report($e);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class RegisterController extends Controller
|
||||
try {
|
||||
Mail::to($user->email)->send(new WelcomeMail($user));
|
||||
} catch (Throwable $e) {
|
||||
Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage());
|
||||
Log::error("Fout bij verzenden van welkomstmail naar {$user->email}: ".$e->getMessage());
|
||||
report($e);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class CartController extends Controller
|
||||
$validated = $request->validate([
|
||||
'model_name' => 'nullable|string|max:255',
|
||||
'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',
|
||||
'material' => 'required|string',
|
||||
'color' => 'required|string',
|
||||
|
||||
@@ -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.'.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class ShopController extends Controller
|
||||
'preferred_material' => 'nullable|string|max:50',
|
||||
'preferred_color' => 'nullable|string|max:50',
|
||||
'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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
use App\Services\DatabaseSchemaHelper;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -23,6 +24,10 @@ class EnsureUserIsAdmin
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,14 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(Order::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Communications / questions submitted by this user.
|
||||
*
|
||||
* @return HasMany<Communication, $this>
|
||||
*/
|
||||
public function communications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Communication::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -548,13 +548,29 @@ class MakerWorldService
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
return $this->extractModelsList($data);
|
||||
$list = $this->extractModelsList($data);
|
||||
if (! empty($list)) {
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("MakerWorld getCollectionDesigns({$collectionId}) error: ".$e->getMessage());
|
||||
}
|
||||
|
||||
return [];
|
||||
// If authenticated and this represents the user collection or favorites:
|
||||
if ($this->isAuthenticated()) {
|
||||
$userFavorites = $this->getFavorites($page, $limit);
|
||||
if (! empty($userFavorites)) {
|
||||
return $userFavorites;
|
||||
}
|
||||
$userPublished = $this->getPublished($page, $limit);
|
||||
if (! empty($userPublished)) {
|
||||
return $userPublished;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to MakerWorld showcase models so the user always receives models in Artikel Beheer!
|
||||
return $this->getCommunityShowcase($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -637,19 +653,14 @@ class MakerWorldService
|
||||
$dimensions = is_array($design['dimensions']) ? implode(' x ', $design['dimensions']) : (string) $design['dimensions'];
|
||||
}
|
||||
|
||||
// Calculate a smart suggested retail price
|
||||
// Base starting price of 9.95, + ~0.08 per gram + 1.25 per print hour
|
||||
$suggestedPrice = 14.95;
|
||||
if ($weightGrams || $printTimeHours) {
|
||||
$cost = 5.0;
|
||||
if ($weightGrams) {
|
||||
$cost += ($weightGrams * 0.07);
|
||||
}
|
||||
if ($printTimeHours) {
|
||||
$cost += ($printTimeHours * 1.50);
|
||||
}
|
||||
$suggestedPrice = round(max(9.95, $cost * 1.6), 2);
|
||||
// Calculate a smart suggested retail price using the 3D Printing Cost Calculator
|
||||
$colorCount = 1;
|
||||
if (! empty($profile['filaments']) && is_array($profile['filaments'])) {
|
||||
$colorCount = max(1, min(4, count($profile['filaments'])));
|
||||
}
|
||||
$calculator = app(PriceCalculatorService::class);
|
||||
$calcResult = $calculator->calculate($weightGrams ?: 85, $printTimeHours ?: 4.0, $colorCount);
|
||||
$suggestedPrice = $calcResult['suggested_price'];
|
||||
|
||||
// Creator and MakerWorld URL
|
||||
$authorName = $design['designCreator']['name'] ?? $design['designCreator']['handle'] ?? 'MakerWorld Creator';
|
||||
@@ -764,10 +775,15 @@ class MakerWorldService
|
||||
if (! $existingProduct->makerworld_collection_id) {
|
||||
$updates['makerworld_collection_id'] = $collection->id;
|
||||
}
|
||||
if (empty($existingProduct->gallery_images) && ! empty($design['designExtension']['design_pictures'])) {
|
||||
$formatted = $this->formatDesignForImport($design);
|
||||
if (empty($existingProduct->gallery_images)) {
|
||||
$full = (! empty($design['designExtension']['design_pictures']))
|
||||
? $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)) {
|
||||
$formatted = $this->formatDesignForImport($design);
|
||||
$updates['print_info'] = $formatted['print_info'];
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\AddSecurityHeaders;
|
||||
use App\Http\Middleware\CheckWebshopOffline;
|
||||
use App\Http\Middleware\EnsureUserIsAdmin;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
@@ -14,6 +16,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->web(append: [
|
||||
AddSecurityHeaders::class,
|
||||
CheckWebshopOffline::class,
|
||||
]);
|
||||
$middleware->alias([
|
||||
'admin' => EnsureUserIsAdmin::class,
|
||||
]);
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'Europe/Amsterdam'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
+9
-1
@@ -43,10 +43,18 @@ return [
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'encryption' => env('MAIL_ENCRYPTION'),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'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' => [
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ namespace Database\Seeders;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Color;
|
||||
use App\Models\MakerWorldCollection;
|
||||
use App\Models\Material;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
@@ -311,7 +312,21 @@ class DatabaseSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($products as $p) {
|
||||
Product::create($p);
|
||||
Product::updateOrCreate(['slug' => $p['slug']], $p);
|
||||
}
|
||||
|
||||
// 4. Standaard Gekoppelde MakerWorld Collectie (@user_1718037659)
|
||||
MakerWorldCollection::updateOrCreate(
|
||||
['makerworld_collection_id' => '1718037659'],
|
||||
[
|
||||
'name' => 'Favorieten & Collecties (@user_1718037659)',
|
||||
'slug' => 'user-1718037659',
|
||||
'description' => 'Automatisch gesynchroniseerde MakerWorld collectie van @user_1718037659 (elke 10 minuten gecontroleerd).',
|
||||
'user_handle' => 'user_1718037659',
|
||||
'source_url' => 'https://makerworld.com/nl/@user_1718037659/collections',
|
||||
'is_active_sync' => true,
|
||||
'sync_interval_minutes' => 10,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-61
@@ -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>
|
||||
<!-- Top Slice Gradients -->
|
||||
<linearGradient id="icoTopFace" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38BDF8" />
|
||||
<stop offset="100%" stop-color="#0284C7" />
|
||||
<linearGradient id="icoTop" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38bdf8" />
|
||||
<stop offset="100%" stop-color="#0284c7" />
|
||||
</linearGradient>
|
||||
<linearGradient id="icoTopRight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0284C7" />
|
||||
<stop offset="100%" stop-color="#0369A1" />
|
||||
<linearGradient id="icoLeft" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1" />
|
||||
<stop offset="100%" stop-color="#4338ca" />
|
||||
</linearGradient>
|
||||
<linearGradient id="icoTopLeft" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0EA5E9" />
|
||||
<stop offset="100%" stop-color="#0284C7" />
|
||||
</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 id="icoRight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0ea5e9" />
|
||||
<stop offset="100%" stop-color="#0369a1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g transform="translate(4, 2)">
|
||||
<!-- Bottom Layer (Amber) -->
|
||||
<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>
|
||||
<!-- Background rounded squircle with subtle border -->
|
||||
<rect x="2" y="2" width="44" height="44" rx="12" fill="#0f172a" />
|
||||
|
||||
<!-- Middle Layer (Rose Pink) -->
|
||||
<g transform="translate(0, 11)">
|
||||
<polygon points="22,6 40,16 22,26 4,16" fill="url(#icoMidFace)" opacity="0.95" />
|
||||
<polygon points="4,16 22,26 22,34 4,24" fill="url(#icoMidLeft)" />
|
||||
<polygon points="22,26 40,16 40,24 22,34" fill="url(#icoMidRight)" />
|
||||
</g>
|
||||
|
||||
<!-- Top Layer (Cyan / Sky) -->
|
||||
<g transform="translate(0, 0)">
|
||||
<polygon points="22,6 40,16 22,26 4,16" fill="url(#icoTopFace)" />
|
||||
<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>
|
||||
<!-- Modern Geometric 3D Prism Mark -->
|
||||
<g transform="translate(6, 6)">
|
||||
<!-- Top Face -->
|
||||
<path d="M18 6 L31 13.5 L18 21 L5 13.5 Z" fill="url(#icoTop)" />
|
||||
<!-- Left Face -->
|
||||
<path d="M5 15.5 L17 22.5 L17 34 L5 27 Z" fill="url(#icoLeft)" />
|
||||
<!-- Right Face -->
|
||||
<path d="M19 22.5 L31 15.5 L31 27 L19 34 Z" fill="url(#icoRight)" />
|
||||
<!-- Precision Core Light Accent -->
|
||||
<circle cx="18" cy="21" r="2" fill="#ffffff" opacity="0.95" />
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.2 KiB |
+35
-94
@@ -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>
|
||||
<!-- Top Slice Gradients -->
|
||||
<linearGradient id="topFace" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38BDF8" />
|
||||
<stop offset="100%" stop-color="#0284C7" />
|
||||
<!-- Modern high-end gradients -->
|
||||
<linearGradient id="facetTop" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38bdf8" />
|
||||
<stop offset="100%" stop-color="#0284c7" />
|
||||
</linearGradient>
|
||||
<linearGradient id="topRight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0284C7" />
|
||||
<stop offset="100%" stop-color="#0369A1" />
|
||||
<linearGradient id="facetLeft" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#6366f1" />
|
||||
<stop offset="100%" stop-color="#4338ca" />
|
||||
</linearGradient>
|
||||
<linearGradient id="topLeft" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0EA5E9" />
|
||||
<stop offset="100%" stop-color="#0284C7" />
|
||||
<linearGradient id="facetRight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0ea5e9" />
|
||||
<stop offset="100%" stop-color="#0369a1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Mid Slice Gradients -->
|
||||
<linearGradient id="midFace" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<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 id="accentDot" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#38bdf8" />
|
||||
<stop offset="100%" stop-color="#6366f1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 3D Layered Isometric Cube Mark -->
|
||||
<g transform="translate(6, 4)">
|
||||
<!-- Bottom Layer (Amber) -->
|
||||
<g transform="translate(0, 22)">
|
||||
<!-- Top surface of bottom layer -->
|
||||
<polygon points="22,6 40,16 22,26 4,16" fill="url(#botFace)" opacity="0.9" />
|
||||
<!-- Left side of bottom layer -->
|
||||
<polygon points="4,16 22,26 22,34 4,24" fill="url(#botLeft)" />
|
||||
<!-- Right side of bottom layer -->
|
||||
<polygon points="22,26 40,16 40,24 22,34" fill="url(#botRight)" />
|
||||
<!-- Modern Geometric 3D Prism Mark -->
|
||||
<g transform="translate(6, 6)">
|
||||
<!-- Top Face (Soft Isometric Diamond) -->
|
||||
<path d="M18 4 L30 11 L18 18 L6 11 Z" fill="url(#facetTop)" />
|
||||
<!-- Left Face (Dimension with 3D print layer notch) -->
|
||||
<path d="M6 13 L17 19.5 L17 31 L6 24.5 Z" fill="url(#facetLeft)" />
|
||||
<!-- Right Face -->
|
||||
<path d="M19 19.5 L30 13 L30 24.5 L19 31 Z" fill="url(#facetRight)" />
|
||||
<!-- Precision Core Light Accent -->
|
||||
<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>
|
||||
|
||||
<!-- Wordmark Text -->
|
||||
<g transform="translate(62, 38)">
|
||||
<!-- "Jouw" in Bold Slate -->
|
||||
<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>
|
||||
|
||||
<!-- "driedee" in Vibrant Gradient -->
|
||||
<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>
|
||||
|
||||
<!-- ".nl" Badge -->
|
||||
<rect x="168" y="-21" width="28" height="16" rx="5" fill="url(#badgeGrad)" />
|
||||
<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>
|
||||
<!-- Modern Clean Typography -->
|
||||
<g transform="translate(50, 31)">
|
||||
<!-- 'jouw' in clean, light-medium neutral font -->
|
||||
<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 -->
|
||||
<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>
|
||||
<!-- Vibrant Modern Dot -->
|
||||
<circle cx="137.5" cy="-5.5" r="3" fill="url(#accentDot)" />
|
||||
<!-- '.nl' micro pill badge -->
|
||||
<rect x="145" y="-17" width="24" height="15" rx="4" fill="#f1f5f9" />
|
||||
<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>
|
||||
</svg>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 2.3 KiB |
+21
-16
@@ -4,8 +4,8 @@
|
||||
@source '../../storage/framework/views/*.php';
|
||||
|
||||
@theme {
|
||||
--font-heading: 'Fredoka', ui-rounded, 'Nunito', system-ui, sans-serif;
|
||||
--font-sans: 'Nunito', system-ui, sans-serif;
|
||||
--font-heading: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -13,22 +13,27 @@
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
/* Refined, smooth playful button */
|
||||
.playful-btn {
|
||||
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
.playful-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.playful-btn:active {
|
||||
transform: translateY(1px);
|
||||
/* Modern subtle glass effect */
|
||||
.glass-header {
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* Modern playful card */
|
||||
.playful-card {
|
||||
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
/* Clean modern micro-elevation */
|
||||
.card-hover {
|
||||
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 {
|
||||
transform: translateY(-3px);
|
||||
.card-hover:hover {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
@section('title', 'Mijn Account | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12 space-y-8" x-data="{ tab: 'orders' }">
|
||||
<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 -->
|
||||
<div class="p-6 sm:p-8 rounded-3xl bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-indigo-500/10 border border-slate-200/80 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-tr from-amber-400 to-rose-400 flex items-center justify-center text-white font-heading font-black text-2xl shadow-xs">
|
||||
<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-3.5">
|
||||
<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)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<h1 class="font-heading text-xl sm:text-2xl font-black text-slate-900">
|
||||
Hallo, {{ $user->name }}!
|
||||
<h1 class="text-xl font-bold text-slate-900">
|
||||
{{ $user->name }}
|
||||
</h1>
|
||||
@if($user->isAdmin())
|
||||
<span class="px-2.5 py-0.5 rounded-full bg-slate-900 text-amber-400 text-[10px] font-black uppercase tracking-wider">
|
||||
Admin Beheerder
|
||||
<span class="px-2 py-0.5 rounded-full bg-slate-100 text-slate-700 text-[10px] font-semibold border border-slate-200">
|
||||
Beheerder
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@@ -28,17 +28,17 @@
|
||||
</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())
|
||||
<a href="{{ route('admin.content.index') }}" class="px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-amber-300 font-black text-xs transition flex items-center space-x-1.5 shadow-xs">
|
||||
<i data-lucide="shield" class="w-4 h-4"></i>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Admin Dashboard</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('logout') }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="px-4 py-2 rounded-xl bg-white hover:bg-rose-50 text-rose-600 border border-slate-200 text-xs font-bold transition flex items-center space-x-1.5 cursor-pointer shadow-2xs">
|
||||
<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>
|
||||
<span>Uitloggen</span>
|
||||
</button>
|
||||
@@ -47,69 +47,85 @@
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
type="button"
|
||||
@click="tab = 'orders'"
|
||||
:class="tab === 'orders' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'"
|
||||
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer"
|
||||
:class="tab === 'orders' ? '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="package" class="w-4 h-4"></i>
|
||||
<span>Mijn Bestellingen ({{ $orders->count() }})</span>
|
||||
<i data-lucide="package" class="w-3.5 h-3.5"></i>
|
||||
<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
|
||||
type="button"
|
||||
@click="tab = 'profile'"
|
||||
:class="tab === 'profile' ? 'bg-amber-400 text-slate-950 font-black' : 'bg-white text-slate-600 hover:bg-slate-100'"
|
||||
class="px-4 py-2 rounded-xl transition flex items-center space-x-1.5 cursor-pointer"
|
||||
:class="tab === 'profile' ? '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="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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB 1: BESTELLINGEN -->
|
||||
<div x-show="tab === 'orders'" class="space-y-4">
|
||||
<div x-show="tab === 'orders'" class="space-y-3">
|
||||
@if($orders->isEmpty())
|
||||
<div class="p-12 text-center rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-3">
|
||||
<div class="w-14 h-14 rounded-2xl bg-amber-50 text-amber-600 flex items-center justify-center mx-auto text-2xl">
|
||||
🛍️
|
||||
<div 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="shopping-bag" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<h3 class="font-heading text-lg font-black text-slate-900">Nog geen bestellingen geplaatst</h3>
|
||||
<p class="text-xs text-slate-500 max-w-sm mx-auto">
|
||||
Je hebt nog geen 3D prints besteld. Ontdek onze fidget toys, Pokémon figuren en popculture modellen!
|
||||
<h3 class="text-sm font-semibold text-slate-900">Nog geen bestellingen</h3>
|
||||
<p class="text-xs text-slate-500 max-w-xs mx-auto">
|
||||
Je hebt nog geen 3D prints besteld. Bekijk onze studio collectie!
|
||||
</p>
|
||||
<div class="pt-2">
|
||||
<a href="{{ route('catalog') }}" class="inline-flex items-center space-x-2 px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-xs font-black transition">
|
||||
<span>Winkelassortiment bekijken</span>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
<a 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>Collectie bekijken</span>
|
||||
<i data-lucide="arrow-right" class="w-3 h-3"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
@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 -->
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-3 border-b border-slate-100 gap-2">
|
||||
<div>
|
||||
<span class="font-mono font-bold text-xs text-slate-900">#{{ $order->order_number }}</span>
|
||||
<span class="text-xs text-slate-400 ml-2">• {{ $order->created_at->format('d-m-Y om H:i') }}</span>
|
||||
<span class="text-xs text-slate-400 ml-2">• {{ $order->created_at->format('d-m-Y H:i') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase
|
||||
@if($order->status === 'completed' || $order->status === 'shipped' || $order->status === 'verzonden') bg-emerald-100 text-emerald-800
|
||||
@elseif($order->status === 'printed' || $order->status === 'processing' || $order->status === 'in_print') bg-sky-100 text-sky-800
|
||||
@elseif($order->status === 'cancelled' || $order->status === 'geannuleerd') bg-rose-100 text-rose-800
|
||||
@else bg-amber-100 text-amber-900 @endif
|
||||
<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
|
||||
">
|
||||
{{ str_replace('_', ' ', $order->status) }}
|
||||
{{ $order->status_label }}
|
||||
</span>
|
||||
|
||||
<span class="font-heading font-black text-slate-900 text-sm">
|
||||
<span class="font-bold text-slate-900 text-xs">
|
||||
{{ $order->formatted_total }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -118,28 +134,28 @@
|
||||
<!-- Items -->
|
||||
<div class="divide-y divide-slate-100">
|
||||
@foreach($order->items as $item)
|
||||
<div class="py-2.5 flex items-start justify-between text-xs">
|
||||
<div class="py-2 flex items-start justify-between text-xs">
|
||||
<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">
|
||||
@if($item->material) Materiaal: <span class="font-semibold">{{ strtoupper($item->material) }}</span> • @endif
|
||||
@if($item->color) Kleur: <span class="font-semibold">{{ $item->color }}</span> • @endif
|
||||
Schaal: {{ $item->scale ?? '100%' }}
|
||||
@if($item->material) {{ strtoupper($item->material) }} • @endif
|
||||
@if($item->color) {{ $item->color }} • @endif
|
||||
{{ $item->scale ?? '100%' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="font-semibold text-slate-600">{{ $item->quantity }}x</span>
|
||||
<span class="font-bold text-slate-900 ml-1">€ {{ number_format($item->total_price, 2, ',', '.') }}</span>
|
||||
<span class="text-slate-500">{{ $item->quantity }}x</span>
|
||||
<span class="font-semibold text-slate-900 ml-1">€ {{ number_format($item->total_price, 2, ',', '.') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Bezorging adres -->
|
||||
<div class="pt-3 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-500">
|
||||
<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 class="font-bold text-emerald-600 flex items-center space-x-1">
|
||||
<i data-lucide="truck" class="w-3.5 h-3.5"></i>
|
||||
<span class="font-medium text-slate-700 flex items-center space-x-1">
|
||||
<i data-lucide="truck" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<span>PostNL</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -150,32 +166,112 @@
|
||||
@endif
|
||||
</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">• {{ $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 -->
|
||||
<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 -->
|
||||
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4">
|
||||
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2">
|
||||
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i>
|
||||
<div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3.5">
|
||||
<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-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>Persoonlijke gegevens</span>
|
||||
</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
|
||||
@method('PUT')
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-700">Naam</label>
|
||||
<input type="text" name="name" value="{{ old('name', $user->name) }}" required class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
|
||||
<div class="space-y-1">
|
||||
<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 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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-700">E-mailadres</label>
|
||||
<input type="email" name="email" value="{{ old('email', $user->email) }}" required class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
|
||||
<div class="space-y-1">
|
||||
<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 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 class="pt-2">
|
||||
<button type="submit" class="px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs transition cursor-pointer">
|
||||
<div class="pt-1">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -183,33 +279,33 @@
|
||||
</div>
|
||||
|
||||
<!-- Wachtwoord Wijzigen -->
|
||||
<div class="p-6 rounded-3xl bg-white border border-slate-200/80 shadow-xs space-y-4">
|
||||
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2">
|
||||
<i data-lucide="lock" class="w-4 h-4 text-rose-500"></i>
|
||||
<div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3.5">
|
||||
<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-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>Wachtwoord wijzigen</span>
|
||||
</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
|
||||
@method('PUT')
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-700">Huidig wachtwoord</label>
|
||||
<input type="password" name="current_password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
|
||||
<div class="space-y-1">
|
||||
<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 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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
|
||||
<input type="password" name="password" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
|
||||
<div class="space-y-1">
|
||||
<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 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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label>
|
||||
<input type="password" name="password_confirmation" required placeholder="••••••••" class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2 text-xs font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400">
|
||||
<div class="space-y-1">
|
||||
<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 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 class="pt-2">
|
||||
<button type="submit" class="px-5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-black text-xs transition cursor-pointer">
|
||||
<div class="pt-1">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -220,4 +316,3 @@
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -6,58 +6,57 @@
|
||||
<div class="space-y-6 max-w-xl mx-auto">
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
<span>Terug naar Categorieën</span>
|
||||
</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>
|
||||
|
||||
<!-- 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
|
||||
@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">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<div class="space-y-1">
|
||||
<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-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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Omschrijving</label>
|
||||
<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 class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<div class="space-y-1">
|
||||
<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-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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<div class="space-y-1">
|
||||
<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-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 class="flex items-center justify-end space-x-3">
|
||||
<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">
|
||||
<div class="flex items-center justify-end space-x-2.5">
|
||||
<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
|
||||
</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">
|
||||
<i data-lucide="save" class="w-4 h-4"></i>
|
||||
<span>Opslaan</span>
|
||||
<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">
|
||||
Wijzigingen Opslaan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -6,41 +6,48 @@
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- 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 class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
|
||||
<i data-lucide="folder-tree" class="w-4 h-4"></i>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Winkelstructuur</span>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-white">Categorieën Beheer</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-400 mt-1">Beheer categorie titels, omschrijvingen en sortering.</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Categorieën</h1>
|
||||
<p class="text-xs text-slate-500 mt-1">Beheer categorietitels, omschrijvingen en sortering van de webshop.</p>
|
||||
</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 -->
|
||||
<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)
|
||||
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 flex flex-col justify-between space-y-4">
|
||||
<div class="space-y-3">
|
||||
<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-2.5">
|
||||
<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 }}
|
||||
</span>
|
||||
<span class="text-xs text-slate-400 font-bold">
|
||||
<span class="text-xs text-slate-500 font-medium">
|
||||
{{ $category->products_count }} producten
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="font-heading text-lg font-black text-white">{{ $category->name }}</h3>
|
||||
<p class="text-xs text-slate-400 leading-relaxed font-normal">{{ $category->description }}</p>
|
||||
<h3 class="text-base font-bold text-slate-900">{{ $category->name }}</h3>
|
||||
<p class="text-xs text-slate-500 leading-relaxed font-normal">{{ $category->description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-800 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">
|
||||
<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-500 hover:text-slate-900 flex items-center space-x-1 transition">
|
||||
<span>Bekijk in shop</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
</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>
|
||||
<span>Bewerken</span>
|
||||
</a>
|
||||
@@ -51,4 +58,3 @@
|
||||
|
||||
</div>
|
||||
@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 & 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 & Tijd</th>
|
||||
<th class="py-3 px-4">Klant</th>
|
||||
<th class="py-3 px-4">Onderwerp & 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') }} • 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"><{{ $communication->email }}></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>•</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 & 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 & 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 →
|
||||
</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 →
|
||||
</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 & 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 & Accounts</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Klanten & 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) • {{ $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">€ {{ 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 & 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>•</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 & 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
|
||||
@@ -3,37 +3,30 @@
|
||||
@section('title', 'MakerWorld Collecties & Auto-Sync | Jouwdriedee.nl Beheer')
|
||||
|
||||
@section('content')
|
||||
<div class="py-8 bg-slate-950 text-slate-100 min-h-screen" x-data="makerWorldSyncApp()">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
|
||||
<div class="space-y-6" x-data="makerWorldSyncApp()">
|
||||
|
||||
<!-- Header & Status Bar -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pb-6 border-b border-slate-800">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 pb-6 border-b border-slate-200/80">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>MakerWorld Integratie</span>
|
||||
<span>•</span>
|
||||
<span>Collecties & 10-Minuten Auto-Sync</span>
|
||||
<div class="inline-flex items-center space-x-2 text-xs font-semibold text-slate-500 mb-1">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
<span>MakerWorld Sync Engine</span>
|
||||
<span class="text-slate-300">•</span>
|
||||
<span>10-minuten automatische synchronisatie</span>
|
||||
</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">
|
||||
<i data-lucide="refresh-cw" class="w-6 h-6"></i>
|
||||
</span>
|
||||
<span>MakerWorld Product Importer & Collecties Sync</span>
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-400">
|
||||
Koppel je MakerWorld collecties (zoals <code class="text-amber-400 font-mono">@user_1718037659</code>). Het systeem controleert elke 10 minuten automatisch op nieuwe modellen!
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900">MakerWorld Product Importer & Collecties</h1>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Koppel MakerWorld collecties van je profiel (<code class="text-slate-800 font-mono bg-slate-100 px-1.5 py-0.5 rounded">@user_1718037659</code>). Nieuwe prints worden elke 10 minuten automatisch binnengehaald.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats & Staging Link -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<a href="{{ route('admin.makerworld.staging') }}" class="px-4 py-2.5 bg-gradient-to-r from-amber-400 to-amber-500 hover:from-amber-500 hover:to-amber-600 text-slate-950 font-black text-xs rounded-xl shadow-md transition flex items-center space-x-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ route('admin.makerworld.staging') }}" class="inline-flex items-center space-x-2 px-3.5 py-2 rounded-xl bg-slate-900 text-white text-xs font-medium hover:bg-slate-800 transition shadow-xs">
|
||||
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||
<span>Artikel Beheer</span>
|
||||
@if($stagedCount > 0)
|
||||
<span class="px-2 py-0.5 rounded-full bg-slate-950 text-amber-300 text-[11px] font-black">
|
||||
{{ $stagedCount }} concepten
|
||||
<span class="px-1.5 py-0.5 rounded-md bg-white/20 text-white text-[10px] font-bold">
|
||||
{{ $stagedCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
@@ -42,40 +35,40 @@
|
||||
|
||||
<!-- Notification Alerts -->
|
||||
@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 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-4 rounded-2xl bg-amber-950/60 border border-amber-500/40 text-amber-200 text-sm flex items-center space-x-3">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-amber-400 shrink-0"></i>
|
||||
<div class="font-bold">{{ session('warning') }}</div>
|
||||
<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
|
||||
|
||||
@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 class="p-3.5 rounded-xl bg-rose-50 border border-rose-200/60 text-rose-800 text-xs flex items-center space-x-2.5">
|
||||
<i data-lucide="x-circle" class="w-4 h-4 text-rose-600 shrink-0"></i>
|
||||
<span class="font-medium">{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 2FA Verificatiecode Invoerscherm (Bambu Lab) -->
|
||||
@if(session('makerworld_2fa_account'))
|
||||
<div class="bg-gradient-to-r from-slate-900 to-amber-950/80 p-6 sm:p-8 rounded-3xl border-2 border-amber-400 shadow-2xl">
|
||||
<div class="max-w-xl mx-auto text-center space-y-4">
|
||||
<div class="w-16 h-16 rounded-2xl bg-amber-400/20 text-amber-400 flex items-center justify-center mx-auto shadow-inner">
|
||||
<i data-lucide="shield-check" class="w-8 h-8"></i>
|
||||
<div class="bg-white p-6 sm:p-8 rounded-2xl border border-slate-200/80 shadow-xs">
|
||||
<div class="max-w-md mx-auto text-center space-y-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-100 text-slate-700 flex items-center justify-center mx-auto">
|
||||
<i data-lucide="shield-check" class="w-6 h-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="px-3 py-1 rounded-full text-xs font-black uppercase tracking-wider bg-amber-400 text-slate-950">
|
||||
Stap 2: Twee-staps Verificatie (2FA)
|
||||
<span class="px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-slate-100 text-slate-700">
|
||||
Tweestapsverificatie (2FA)
|
||||
</span>
|
||||
<h2 class="text-2xl font-black text-white mt-2">Bambu Lab Verificatiecode</h2>
|
||||
<p class="text-xs text-slate-300 mt-1">
|
||||
Vul de 6-cijferige verificatiecode in die Bambu Lab zojuist heeft verzonden naar <strong class="text-amber-300 font-mono">{{ session('makerworld_2fa_account') }}</strong>.
|
||||
<h2 class="text-xl font-bold text-slate-900 mt-2">Bambu Lab Beveiligingscode</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Vul de 6-cijferige verificatiecode in die verzonden is naar <strong class="text-slate-800 font-mono">{{ session('makerworld_2fa_account') }}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -91,123 +84,123 @@
|
||||
placeholder="123456"
|
||||
autofocus
|
||||
required
|
||||
class="w-56 text-center text-3xl font-mono font-black tracking-widest px-4 py-3 bg-slate-950 border-2 border-amber-400 rounded-2xl text-amber-300 focus:outline-none focus:ring-4 focus:ring-amber-400/30 placeholder-slate-700"
|
||||
class="w-48 text-center text-2xl font-mono font-bold tracking-widest px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 placeholder-slate-400"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3 pt-2">
|
||||
<button type="submit" class="px-8 py-3 bg-amber-400 hover:bg-amber-300 text-slate-950 font-black rounded-2xl text-xs uppercase tracking-wider shadow-lg shadow-amber-400/20 transition flex items-center space-x-2">
|
||||
<button type="submit" class="px-6 py-2.5 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-xl text-xs transition flex items-center space-x-2 shadow-xs">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
<span>Verifieer Code & Inloggen</span>
|
||||
<span>Verifieer & Koppel</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-3 border-t border-slate-800/80 flex items-center justify-center space-x-6 text-xs text-slate-400">
|
||||
<div class="pt-3 border-t border-slate-100 flex items-center justify-center space-x-4 text-xs text-slate-500">
|
||||
<form action="{{ route('admin.makerworld.resend-code') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="account" value="{{ session('makerworld_2fa_account') }}">
|
||||
<button type="submit" class="hover:text-amber-400 underline font-bold transition">
|
||||
Geen code ontvangen? Verstuur opnieuw
|
||||
<button type="submit" class="hover:text-slate-900 underline font-medium transition">
|
||||
Code opnieuw verzenden
|
||||
</button>
|
||||
</form>
|
||||
<span>•</span>
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'auth']) }}" class="hover:text-slate-300 underline font-bold transition">
|
||||
Ander account proberen
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'auth']) }}" class="hover:text-slate-900 underline font-medium transition">
|
||||
Ander account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- TAB Bar -->
|
||||
<div class="flex items-center space-x-2 border-b border-slate-800 pb-2 overflow-x-auto text-sm font-bold">
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-2 {{ $currentTab === 'collections' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
|
||||
<i data-lucide="folder-sync" class="w-4 h-4"></i>
|
||||
<!-- TAB Bar: Subtle Segmented Control -->
|
||||
<div class="flex items-center space-x-1.5 p-1 bg-slate-100/80 rounded-xl max-w-max text-xs font-medium">
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'collections']) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentTab === 'collections' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||
<i data-lucide="folder-sync" class="w-3.5 h-3.5"></i>
|
||||
<span>Gekoppelde Collecties</span>
|
||||
<span class="px-2 py-0.5 rounded-md text-xs {{ $currentTab === 'collections' ? 'bg-slate-950 text-amber-400' : 'bg-slate-800 text-slate-400' }}">
|
||||
<span class="px-1.5 py-0.2 rounded-md text-[10px] {{ $currentTab === 'collections' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/60 text-slate-600' }}">
|
||||
{{ $trackedCollections->count() }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.makerworld.staging') }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-2 text-slate-400 hover:text-white hover:bg-slate-900">
|
||||
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||
<span>Artikel Beheer (Staging)</span>
|
||||
<a href="{{ route('admin.makerworld.staging') }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 text-slate-600 hover:text-slate-900">
|
||||
<i data-lucide="layers" class="w-3.5 h-3.5"></i>
|
||||
<span>Artikel Beheer</span>
|
||||
@if($stagedCount > 0)
|
||||
<span class="px-2 py-0.5 rounded-md text-xs bg-amber-400 text-slate-950 font-black">
|
||||
{{ $stagedCount }} nieuw
|
||||
<span class="px-1.5 py-0.2 rounded-md text-[10px] bg-slate-900 text-white font-semibold">
|
||||
{{ $stagedCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'quick']) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-2 {{ $currentTab === 'quick' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
|
||||
<i data-lucide="zap" class="w-4 h-4"></i>
|
||||
<span>Los Model Importeren</span>
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'quick']) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentTab === 'quick' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||
<i data-lucide="zap" class="w-3.5 h-3.5"></i>
|
||||
<span>Los Model</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'auth']) }}" class="px-4 py-2 rounded-xl transition flex items-center space-x-2 {{ $currentTab === 'auth' ? 'bg-amber-400 text-slate-950 font-black' : 'text-slate-400 hover:text-white hover:bg-slate-900' }}">
|
||||
<i data-lucide="key" class="w-4 h-4"></i>
|
||||
<span>MakerWorld Login & Token</span>
|
||||
<a href="{{ route('admin.makerworld.index', ['tab' => 'auth']) }}" class="px-3 py-1.5 rounded-lg transition flex items-center space-x-1.5 {{ $currentTab === 'auth' ? 'bg-white text-slate-900 font-semibold shadow-2xs' : 'text-slate-600 hover:text-slate-900' }}">
|
||||
<i data-lucide="key" class="w-3.5 h-3.5"></i>
|
||||
<span>MakerWorld Login</span>
|
||||
@if($isAuthenticated)
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- TAB 1: COLLECTIONS & AUTO-SYNC -->
|
||||
@if($currentTab === 'collections')
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Add Collection Form -->
|
||||
<div class="bg-slate-900/90 rounded-3xl p-6 sm:p-8 border border-slate-800 shadow-xl">
|
||||
<div class="bg-white rounded-2xl p-6 border border-slate-200/80 shadow-xs">
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex items-center space-x-2 text-xs font-bold uppercase tracking-wider text-amber-400 mb-1">
|
||||
<i data-lucide="plus-circle" class="w-4 h-4"></i>
|
||||
<span>Nieuwe Collectie Toevoegen</span>
|
||||
<div class="flex items-center space-x-2 text-xs font-semibold text-slate-700 mb-1">
|
||||
<i data-lucide="plus" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>Nieuwe collectie koppelen</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-black text-white">MakerWorld Collectie Koppelen aan Auto-Sync</h2>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Voer de link in van een collectie (bijv. <code class="text-amber-300 font-mono">https://makerworld.com/nl/collections/9205051</code>) of je gebruikersprofiel <code class="text-amber-300 font-mono">https://makerworld.com/nl/@user_1718037659/collections</code>.
|
||||
<h2 class="text-base font-bold text-slate-900">MakerWorld Collectie toevoegen aan Auto-Sync</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">
|
||||
Voer een collectie URL in of je gebruikersprofiel <code class="text-slate-700 font-mono bg-slate-100 px-1 py-0.5 rounded">https://makerworld.com/nl/@user_1718037659/collections</code>.
|
||||
</p>
|
||||
|
||||
<form action="{{ route('admin.makerworld.collections.add') }}" method="POST" class="mt-6 space-y-4">
|
||||
<form action="{{ route('admin.makerworld.collections.add') }}" method="POST" class="mt-4 space-y-3.5">
|
||||
@csrf
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-300 uppercase tracking-wider mb-2">
|
||||
Collectie URL, Model Collectie-ID of Gebruikersprofiel
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1.5">
|
||||
Collectie URL, ID of Gebruikersprofiel
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-500">
|
||||
<i data-lucide="link" class="w-5 h-5"></i>
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
|
||||
<i data-lucide="link" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="collection_input"
|
||||
value="{{ old('collection_input', 'https://makerworld.com/nl/@user_1718037659/collections') }}"
|
||||
placeholder="bijv. https://makerworld.com/nl/@user_1718037659/collections of 9205051..."
|
||||
class="w-full pl-11 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-2xl text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-amber-400 text-sm font-mono"
|
||||
placeholder="bijv. https://makerworld.com/nl/@user_1718037659/collections"
|
||||
class="w-full pl-9 pr-3.5 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 text-xs font-mono"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-2">
|
||||
<div class="flex items-center space-x-6 text-xs text-slate-300">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-1">
|
||||
<div class="flex items-center space-x-4 text-xs text-slate-600">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" name="is_active_sync" value="1" checked class="w-4 h-4 rounded text-amber-500 bg-slate-950 border-slate-700 focus:ring-amber-400">
|
||||
<span class="font-bold">Iedere 10 minuten automatisch controleren</span>
|
||||
<input type="checkbox" name="is_active_sync" value="1" checked class="w-4 h-4 rounded text-slate-900 border-slate-300 focus:ring-slate-900">
|
||||
<span>Elke 10 minuten synchroniseren</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" name="sync_now" value="1" checked class="w-4 h-4 rounded text-amber-500 bg-slate-950 border-slate-700 focus:ring-amber-400">
|
||||
<span class="font-bold">Direct eerste import uitvoeren</span>
|
||||
<input type="checkbox" name="sync_now" value="1" checked class="w-4 h-4 rounded text-slate-900 border-slate-300 focus:ring-slate-900">
|
||||
<span>Direct importeren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-6 py-3 bg-amber-400 hover:bg-amber-300 text-slate-950 font-black rounded-2xl text-xs uppercase tracking-wider shadow-lg shadow-amber-400/20 transition flex items-center justify-center space-x-2 shrink-0">
|
||||
<i data-lucide="download-cloud" class="w-4 h-4"></i>
|
||||
<span>Collectie Koppelen</span>
|
||||
<button type="submit" class="px-4 py-2 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-xl text-xs transition flex items-center justify-center space-x-1.5 shadow-xs shrink-0 cursor-pointer">
|
||||
<i data-lucide="download" class="w-3.5 h-3.5"></i>
|
||||
<span>Koppelen & Synchroniseren</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -215,101 +208,101 @@
|
||||
</div>
|
||||
|
||||
<!-- Tracked Collections List -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-black text-white flex items-center space-x-2">
|
||||
<i data-lucide="check-check" class="w-5 h-5 text-emerald-400"></i>
|
||||
<span>Actief Gevolgde Collecties (Auto-Sync)</span>
|
||||
</h2>
|
||||
<p class="text-xs text-slate-400">
|
||||
Modellen uit deze collecties worden binnengehaald als concepten in <a href="{{ route('admin.makerworld.staging') }}" class="text-amber-400 underline font-bold">Artikel Beheer</a>.
|
||||
<h2 class="text-sm font-bold text-slate-900">Actief gevolgde collecties ({{ $trackedCollections->count() }})</h2>
|
||||
<p class="text-xs text-slate-500">
|
||||
Prints uit deze collecties komen binnen als concepten in <a href="{{ route('admin.makerworld.staging') }}" class="text-slate-900 underline font-medium">Artikel Beheer</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($trackedCollections->isEmpty())
|
||||
<div class="text-center py-12 bg-slate-900/60 rounded-3xl border border-slate-800 p-8">
|
||||
<div class="w-16 h-16 rounded-2xl bg-slate-800 flex items-center justify-center text-slate-400 mx-auto mb-4">
|
||||
<i data-lucide="folder-x" class="w-8 h-8"></i>
|
||||
<div class="text-center py-10 bg-white rounded-2xl border border-slate-200/80 p-6">
|
||||
<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="folder-x" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-white">Nog geen collecties gekoppeld</h3>
|
||||
<p class="text-xs text-slate-400 max-w-md mx-auto mt-1">
|
||||
Voeg hierboven een collectie toe of gebruik je gebruikersnaam <code class="text-amber-400 font-mono">@user_1718037659</code> om automatische 10-minuten import te starten.
|
||||
<h3 class="text-xs font-semibold text-slate-800">Nog geen collecties gekoppeld</h3>
|
||||
<p class="text-xs text-slate-500 max-w-sm mx-auto mt-0.5">
|
||||
Koppel hierboven je profiel om automatische 10-minuten import te starten.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@foreach($trackedCollections as $collection)
|
||||
<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">
|
||||
<div class="bg-white rounded-2xl border border-slate-200/80 overflow-hidden flex flex-col justify-between shadow-2xs hover:border-slate-300 transition">
|
||||
<div>
|
||||
<!-- Collection Header & Cover -->
|
||||
<div class="relative h-40 bg-slate-950 overflow-hidden">
|
||||
<!-- Cover -->
|
||||
<div class="relative h-36 bg-slate-100 overflow-hidden">
|
||||
@if($collection->cover_url)
|
||||
<img src="{{ $collection->cover_url }}" alt="{{ $collection->name }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center bg-gradient-to-tr from-slate-900 to-slate-800 text-slate-600">
|
||||
<i data-lucide="image" class="w-12 h-12"></i>
|
||||
<div class="w-full h-full flex items-center justify-center bg-slate-100 text-slate-400">
|
||||
<i data-lucide="image" class="w-8 h-8"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-slate-900 via-transparent to-transparent"></div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-slate-950/70 via-transparent to-transparent"></div>
|
||||
|
||||
<!-- Auto-sync badge -->
|
||||
<div class="absolute top-3 right-3">
|
||||
<!-- Badge -->
|
||||
<div class="absolute top-2.5 right-2.5">
|
||||
@if($collection->is_active_sync)
|
||||
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider bg-emerald-500/90 text-white backdrop-blur flex items-center space-x-1 shadow-md">
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-500/90 text-white backdrop-blur flex items-center space-x-1 shadow-xs">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-white animate-pulse"></span>
|
||||
<span>Auto-Sync 10m</span>
|
||||
<span>10m Sync</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase tracking-wider bg-slate-800 text-slate-400 backdrop-blur border border-slate-700">
|
||||
<span class="px-2 py-0.5 rounded-full text-[10px] font-medium bg-slate-900/80 text-slate-300 backdrop-blur">
|
||||
Gepauzeerd
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-3 left-3 right-3">
|
||||
<div class="text-[10px] font-bold text-amber-400 uppercase tracking-wider">
|
||||
<div class="absolute bottom-2.5 left-3 right-3 text-white">
|
||||
<div class="text-[10px] text-slate-300 font-mono">
|
||||
ID: #{{ $collection->makerworld_collection_id }} • {{ $collection->user_handle ?? 'MakerWorld' }}
|
||||
</div>
|
||||
<h3 class="text-base font-black text-white truncate" title="{{ $collection->name }}">
|
||||
<h3 class="text-sm font-bold truncate" title="{{ $collection->name }}">
|
||||
{{ $collection->name }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body Stats -->
|
||||
<div class="p-5 space-y-3">
|
||||
<div class="p-4 space-y-3">
|
||||
@if($collection->description)
|
||||
<p class="text-xs text-slate-400 line-clamp-2">
|
||||
<p class="text-xs text-slate-500 line-clamp-2">
|
||||
{{ $collection->description }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 py-2 px-3 bg-slate-950 rounded-2xl border border-slate-800 text-center">
|
||||
<div class="grid grid-cols-3 gap-2 py-2 px-3 bg-slate-50 rounded-xl text-center text-xs">
|
||||
<div>
|
||||
<div class="text-[10px] font-bold text-slate-500 uppercase">Totaal</div>
|
||||
<div class="text-sm font-black text-white">{{ $collection->items_count }}</div>
|
||||
<div class="text-[10px] text-slate-400 font-medium">Totaal</div>
|
||||
<div class="font-bold text-slate-800">{{ $collection->items_count }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] font-bold text-amber-400 uppercase">Concept</div>
|
||||
<div class="text-sm font-black text-amber-400">{{ $collection->draft_products_count }}</div>
|
||||
<div class="text-[10px] text-amber-600 font-medium">Concept</div>
|
||||
<div class="font-bold text-amber-700">{{ $collection->draft_products_count }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[10px] font-bold text-emerald-400 uppercase">Live</div>
|
||||
<div class="text-sm font-black text-emerald-400">{{ $collection->published_products_count }}</div>
|
||||
<div class="text-[10px] text-emerald-600 font-medium">Live</div>
|
||||
<div class="font-bold text-emerald-700">{{ $collection->published_products_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[11px] text-slate-400 space-y-1">
|
||||
<div class="text-[11px] text-slate-500 space-y-0.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Laatste controle:</span>
|
||||
<span class="font-mono text-slate-300">
|
||||
{{ $collection->last_synced_at ? $collection->last_synced_at->diffForHumans() : 'Nog niet' }}
|
||||
<span class="font-mono text-slate-700 font-medium">
|
||||
{{ $collection->last_synced_at ? $collection->last_synced_at->timezone(config('app.timezone', 'Europe/Amsterdam'))->diffForHumans() : 'Nog niet' }}
|
||||
@if($collection->last_synced_at)
|
||||
<span class="text-[10px] text-slate-400 font-normal">({{ $collection->last_synced_at->timezone(config('app.timezone', 'Europe/Amsterdam'))->format('H:i') }})</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@if($collection->last_sync_message)
|
||||
<div class="text-[10px] text-slate-500 truncate" title="{{ $collection->last_sync_message }}">
|
||||
<div class="text-[10px] text-slate-400 truncate" title="{{ $collection->last_sync_message }}">
|
||||
Status: {{ $collection->last_sync_message }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -318,32 +311,29 @@
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="p-4 bg-slate-950/60 border-t border-slate-800 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Manual Sync Now -->
|
||||
<div class="p-3 bg-slate-50/60 border-t border-slate-100 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<form action="{{ route('admin.makerworld.collections.sync', $collection) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" title="Nu direct synchroniseren" class="px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-white text-xs font-bold transition flex items-center space-x-1 border border-slate-700">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-amber-400"></i>
|
||||
<span>Nu Syncen</span>
|
||||
<button type="submit" class="px-2.5 py-1.5 rounded-lg bg-white border border-slate-200 text-slate-700 hover:text-slate-900 text-xs font-medium transition flex items-center space-x-1 cursor-pointer">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3 text-slate-500"></i>
|
||||
<span>Sync nu</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Toggle Active Auto-sync -->
|
||||
<form action="{{ route('admin.makerworld.collections.toggle', $collection) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" title="{{ $collection->is_active_sync ? 'Auto-sync pauzeren' : 'Auto-sync aanzetten' }}" class="px-2.5 py-1.5 rounded-xl text-xs font-bold transition border {{ $collection->is_active_sync ? 'bg-amber-400/10 text-amber-400 border-amber-400/30 hover:bg-amber-400/20' : 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 hover:bg-emerald-500/20' }}">
|
||||
<button type="submit" class="px-2 py-1.5 rounded-lg text-xs font-medium transition border cursor-pointer {{ $collection->is_active_sync ? 'bg-white text-slate-600 border-slate-200 hover:text-slate-900' : 'bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100' }}">
|
||||
{{ $collection->is_active_sync ? 'Pauzeren' : 'Inschakelen' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete Collection -->
|
||||
<form action="{{ route('admin.makerworld.collections.delete', $collection) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je deze collectie wilt ontkoppelen?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" title="Collectie verwijderen" class="p-2 rounded-xl text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 transition">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<button type="submit" title="Collectie verwijderen" class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 transition cursor-pointer">
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -356,51 +346,49 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- TAB: COMMUNITY / FAVORITES SHOWCASE -->
|
||||
<!-- TAB: COMMUNITY & POPULAIRE MODELLEN -->
|
||||
@if(in_array($currentTab, ['community', 'favorites']))
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-black text-white flex items-center space-x-2">
|
||||
<i data-lucide="compass" class="w-5 h-5 text-amber-400"></i>
|
||||
<span>MakerWorld Modellen Showcase</span>
|
||||
</h2>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Selecteer modellen om direct toe te voegen aan je webshop catalogus.
|
||||
</p>
|
||||
<h2 class="text-base font-bold text-slate-900">Populaire MakerWorld Modellen</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Direct importeren naar je catalogus of artikelbeheer.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
@forelse($models as $model)
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@forelse($models ?? [] as $model)
|
||||
@php
|
||||
$mId = $model['id'] ?? 0;
|
||||
$cover = $model['cover'] ?? $model['coverUrl'] ?? 'https://images.unsplash.com/photo-1581092160607-ee22621dd758?auto=format&fit=crop&w=600&q=80';
|
||||
$cover = $model['cover'] ?? $model['coverUrl'] ?? '';
|
||||
$title = $model['title'] ?? 'MakerWorld Model #' . $mId;
|
||||
$author = $model['designCreator']['name'] ?? $model['designCreator']['handle'] ?? 'MakerWorld Maker';
|
||||
$author = $model['designCreator']['name'] ?? $model['designCreator']['handle'] ?? 'MakerWorld';
|
||||
@endphp
|
||||
<div class="bg-slate-900 rounded-3xl border border-slate-800 overflow-hidden flex flex-col justify-between shadow-lg">
|
||||
<div class="relative aspect-4/3 overflow-hidden bg-slate-950">
|
||||
<div class="bg-white rounded-2xl border border-slate-200/80 overflow-hidden shadow-2xs flex flex-col justify-between">
|
||||
<div class="aspect-4/3 bg-slate-100 overflow-hidden relative">
|
||||
@if($cover)
|
||||
<img src="{{ $cover }}" alt="{{ $title }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full flex items-center justify-center text-slate-300">
|
||||
<i data-lucide="image" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<div class="p-4 flex flex-col flex-1">
|
||||
<div class="text-[10px] text-slate-500 mb-1 flex items-center justify-between">
|
||||
<span class="truncate">Door {{ $author }}</span>
|
||||
<span class="font-mono">#{{ $mId }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3.5 space-y-2 flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="text-[10px] text-slate-400 font-mono">#{{ $mId }} • {{ $author }}</div>
|
||||
<h3 class="text-xs font-semibold text-slate-900 line-clamp-2 mt-0.5" title="{{ $title }}">{{ $title }}</h3>
|
||||
</div>
|
||||
<h3 class="text-sm font-bold text-white line-clamp-2 leading-snug mb-3 flex-1">
|
||||
{{ $title }}
|
||||
</h3>
|
||||
<form action="{{ route('admin.makerworld.import') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="makerworld_id" value="{{ $mId }}">
|
||||
<button type="submit" class="w-full py-2 px-3 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs uppercase tracking-wider transition">
|
||||
Importeer Model
|
||||
<button type="submit" class="w-full py-1.5 px-3 rounded-lg bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs transition cursor-pointer shadow-xs">
|
||||
Importeren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full py-12 text-center text-slate-400 text-xs">
|
||||
Geen modellen gevonden in deze weergave.
|
||||
<div class="col-span-full py-12 text-center text-slate-400 text-xs bg-white rounded-2xl border border-slate-200">
|
||||
Geen modellen gevonden in dit overzicht.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@@ -409,108 +397,108 @@
|
||||
|
||||
<!-- TAB 2: QUICK SINGLE MODEL IMPORT -->
|
||||
@if($currentTab === 'quick')
|
||||
<div class="bg-slate-900/90 rounded-3xl p-6 sm:p-8 border border-slate-800 shadow-xl space-y-6">
|
||||
<div class="bg-white rounded-2xl p-6 border border-slate-200/80 shadow-xs space-y-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-black text-white flex items-center space-x-2">
|
||||
<i data-lucide="zap" class="w-5 h-5 text-amber-400"></i>
|
||||
<span>Los Model Importeren via MakerWorld URL of ID</span>
|
||||
<h2 class="text-base font-bold text-slate-900 flex items-center space-x-2">
|
||||
<i data-lucide="zap" class="w-4 h-4 text-slate-700"></i>
|
||||
<span>Los model importeren</span>
|
||||
</h2>
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
Plak de URL van een specifiek model op MakerWorld (bijv. <code class="text-amber-300 font-mono">https://makerworld.com/nl/models/55866</code>) om het direct op te halen met alle foto's en printgegevens.
|
||||
<p class="text-xs text-slate-500 mt-0.5">
|
||||
Plak de URL van een specifiek MakerWorld model (bijv. <code class="text-slate-700 font-mono bg-slate-100 px-1 py-0.5 rounded">https://makerworld.com/nl/models/55866</code>) om het direct op te halen met alle foto's en printgegevens.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="fetchQuickPreview()" class="flex flex-col sm:flex-row gap-3">
|
||||
<form @submit.prevent="fetchQuickPreview()" class="flex flex-col sm:flex-row gap-2.5">
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-500">
|
||||
<i data-lucide="link" class="w-5 h-5"></i>
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
|
||||
<i data-lucide="link" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
x-model="quickInput"
|
||||
placeholder="https://makerworld.com/nl/models/55866 of 55866..."
|
||||
class="w-full pl-11 pr-4 py-3 bg-slate-950 border border-slate-800 rounded-2xl text-white placeholder-slate-600 focus:outline-none focus:ring-2 focus:ring-amber-400 text-sm font-mono"
|
||||
class="w-full pl-9 pr-3.5 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900 text-xs font-mono"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoadingQuick"
|
||||
class="px-6 py-3 bg-amber-400 hover:bg-amber-300 disabled:opacity-50 text-slate-950 font-black rounded-2xl text-xs uppercase tracking-wider flex items-center justify-center space-x-2 shadow-lg shadow-amber-400/20 transition shrink-0"
|
||||
class="px-4 py-2.5 bg-slate-900 hover:bg-slate-800 disabled:opacity-50 text-white font-medium rounded-xl text-xs flex items-center justify-center space-x-1.5 shadow-xs transition shrink-0 cursor-pointer"
|
||||
>
|
||||
<template x-if="isLoadingQuick">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i>
|
||||
<span>Ophalen...</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!isLoadingQuick">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
<span>Gegevens Bekijken</span>
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<i data-lucide="search" class="w-3.5 h-3.5"></i>
|
||||
<span>Gegevens ophalen</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Preview Area -->
|
||||
<div x-show="quickPreview" x-cloak class="mt-6 p-6 rounded-3xl bg-slate-950 border border-slate-800">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div x-show="quickPreview" x-cloak class="mt-4 p-5 rounded-2xl bg-slate-50 border border-slate-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<div>
|
||||
<img :src="quickPreview?.model?.image_url" alt="" class="w-full aspect-square object-cover rounded-2xl bg-slate-900 border border-slate-800">
|
||||
<div class="mt-2 flex gap-2 overflow-x-auto py-1">
|
||||
<img :src="quickPreview?.model?.image_url" alt="" class="w-full aspect-square object-cover rounded-xl bg-white border border-slate-200">
|
||||
<div class="mt-2 flex gap-1.5 overflow-x-auto py-1">
|
||||
<template x-for="(pic, idx) in quickPreview?.model?.gallery_images" :key="idx">
|
||||
<img :src="pic" alt="" class="w-12 h-12 object-cover rounded-lg border border-slate-800 shrink-0">
|
||||
<img :src="pic" alt="" class="w-10 h-10 object-cover rounded-lg border border-slate-200 shrink-0">
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2 space-y-4">
|
||||
<div class="md:col-span-2 space-y-3">
|
||||
<div>
|
||||
<div class="text-xs text-amber-400 font-bold uppercase tracking-wider" x-text="'MakerWorld #' + quickPreview?.model?.makerworld_id"></div>
|
||||
<h3 class="text-xl font-black text-white" x-text="quickPreview?.model?.name"></h3>
|
||||
<p class="text-xs text-slate-400 mt-1" x-text="'Maker: ' + quickPreview?.model?.makerworld_author"></p>
|
||||
<div class="text-[11px] font-semibold text-slate-500 font-mono" x-text="'MakerWorld #' + quickPreview?.model?.makerworld_id"></div>
|
||||
<h3 class="text-base font-bold text-slate-900" x-text="quickPreview?.model?.name"></h3>
|
||||
<p class="text-xs text-slate-500" x-text="'Maker: ' + quickPreview?.model?.makerworld_author"></p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3 p-3 rounded-2xl bg-slate-900 border border-slate-800 text-xs">
|
||||
<div class="grid grid-cols-3 gap-2.5 p-3 rounded-xl bg-white border border-slate-200 text-xs">
|
||||
<div>
|
||||
<span class="text-slate-500 block">Printtijd</span>
|
||||
<span class="font-bold text-white" x-text="(quickPreview?.model?.print_time_hours || '?') + ' uur'"></span>
|
||||
<span class="text-slate-400 block text-[10px]">Printtijd</span>
|
||||
<span class="font-bold text-slate-800" x-text="(quickPreview?.model?.print_time_hours || '?') + ' uur'"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-slate-500 block">Gewicht</span>
|
||||
<span class="font-bold text-white" x-text="(quickPreview?.model?.weight_grams || '?') + ' gram'"></span>
|
||||
<span class="text-slate-400 block text-[10px]">Gewicht</span>
|
||||
<span class="font-bold text-slate-800" x-text="(quickPreview?.model?.weight_grams || '?') + ' gram'"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-slate-500 block">Adviesprijs</span>
|
||||
<span class="font-black text-amber-400" x-text="'€ ' + quickPreview?.model?.base_price"></span>
|
||||
<span class="text-slate-400 block text-[10px]">Adviesprijs</span>
|
||||
<span class="font-bold text-slate-900" x-text="'€ ' + quickPreview?.model?.base_price"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import form -->
|
||||
<form action="{{ route('admin.makerworld.import') }}" method="POST" class="pt-4 border-t border-slate-800 flex flex-wrap gap-4 items-end">
|
||||
<form action="{{ route('admin.makerworld.import') }}" method="POST" class="pt-3 border-t border-slate-200 flex flex-wrap gap-3 items-end">
|
||||
@csrf
|
||||
<input type="hidden" name="makerworld_id" :value="quickPreview?.model?.makerworld_id">
|
||||
|
||||
<div class="w-48">
|
||||
<label class="block text-[11px] font-bold text-slate-400 uppercase mb-1">Categorie</label>
|
||||
<select name="category_id" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
|
||||
<div class="w-44">
|
||||
<label class="block text-[11px] font-medium text-slate-600 mb-1">Categorie</label>
|
||||
<select name="category_id" class="w-full px-2.5 py-1.5 bg-white border border-slate-200 rounded-lg text-slate-900 text-xs">
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}">{{ $cat->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="w-32">
|
||||
<label class="block text-[11px] font-bold text-slate-400 uppercase mb-1">Verkoopprijs (€)</label>
|
||||
<input type="number" step="0.01" name="custom_price" :value="quickPreview?.model?.base_price" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400">
|
||||
<div class="w-28">
|
||||
<label class="block text-[11px] font-medium text-slate-600 mb-1">Prijs (€)</label>
|
||||
<input type="number" step="0.01" name="custom_price" :value="quickPreview?.model?.base_price" class="w-full px-2.5 py-1.5 bg-white border border-slate-200 rounded-lg text-slate-900 text-xs font-semibold">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="submit" name="status" value="published" class="px-5 py-2.5 bg-emerald-500 hover:bg-emerald-400 text-white font-black rounded-xl text-xs uppercase tracking-wider transition">
|
||||
✓ Direct Publiceren
|
||||
<div class="flex items-center space-x-2">
|
||||
<button type="submit" name="status" value="published" class="px-3.5 py-1.5 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-lg text-xs transition shadow-xs cursor-pointer">
|
||||
Direct Publiceren
|
||||
</button>
|
||||
<button type="submit" name="status" value="draft" class="px-5 py-2.5 bg-slate-800 hover:bg-slate-700 text-amber-300 font-bold rounded-xl text-xs uppercase tracking-wider transition border border-slate-700">
|
||||
<button type="submit" name="status" value="draft" class="px-3.5 py-1.5 bg-white hover:bg-slate-50 text-slate-700 font-medium rounded-lg text-xs transition border border-slate-200 cursor-pointer">
|
||||
Naar Concepten
|
||||
</button>
|
||||
</div>
|
||||
@@ -523,70 +511,70 @@
|
||||
|
||||
<!-- TAB 3: AUTH & CREDENTIALS -->
|
||||
@if($currentTab === 'auth')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Login with MakerWorld Credentials -->
|
||||
<div class="bg-slate-900/90 rounded-3xl p-6 sm:p-8 border border-slate-800 shadow-xl space-y-4">
|
||||
<div class="bg-white rounded-2xl p-6 border border-slate-200/80 shadow-xs space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 rounded-2xl bg-amber-400/10 text-amber-400 flex items-center justify-center font-bold">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-700 flex items-center justify-center">
|
||||
<i data-lucide="shield-check" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-black text-white">Inloggen met MakerWorld</h2>
|
||||
<p class="text-xs text-slate-400">Bambu Lab / MakerWorld inloggegevens</p>
|
||||
<h2 class="text-sm font-bold text-slate-900">Inloggen met MakerWorld</h2>
|
||||
<p class="text-xs text-slate-500">Bambu Lab / MakerWorld inloggegevens</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.makerworld.login') }}" method="POST" class="space-y-4 pt-2">
|
||||
<form action="{{ route('admin.makerworld.login') }}" method="POST" class="space-y-3 pt-1">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-300 uppercase mb-1">E-mailadres MakerWorld</label>
|
||||
<input type="text" name="account" value="{{ old('account', 'info@jouwdriedee.nl') }}" class="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-xl text-white text-sm" required>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">E-mailadres MakerWorld</label>
|
||||
<input type="text" name="account" value="{{ old('account', 'info@jouwdriedee.nl') }}" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-300 uppercase mb-1">Wachtwoord</label>
|
||||
<input type="password" name="password" class="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-xl text-white text-sm" required>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Wachtwoord</label>
|
||||
<input type="password" name="password" class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-3 bg-amber-400 hover:bg-amber-300 text-slate-950 font-black rounded-xl text-xs uppercase tracking-wider transition">
|
||||
<button type="submit" class="w-full py-2.5 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-xl text-xs transition shadow-xs cursor-pointer">
|
||||
Inloggen bij MakerWorld
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Or Paste Token Directly -->
|
||||
<div class="bg-slate-900/90 rounded-3xl p-6 sm:p-8 border border-slate-800 shadow-xl space-y-4">
|
||||
<div class="bg-white rounded-2xl p-6 border border-slate-200/80 shadow-xs space-y-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-10 h-10 rounded-2xl bg-sky-500/10 text-sky-400 flex items-center justify-center font-bold">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-700 flex items-center justify-center">
|
||||
<i data-lucide="key" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-black text-white">MakerWorld Bearer Token</h2>
|
||||
<p class="text-xs text-slate-400">Plak direct je MakerWorld JWT token</p>
|
||||
<h2 class="text-sm font-bold text-slate-900">MakerWorld Bearer Token</h2>
|
||||
<p class="text-xs text-slate-500">Plak direct je MakerWorld JWT token</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.makerworld.token') }}" method="POST" class="space-y-4 pt-2">
|
||||
<form action="{{ route('admin.makerworld.token') }}" method="POST" class="space-y-3 pt-1">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-300 uppercase mb-1">Access Token</label>
|
||||
<textarea name="token" rows="4" placeholder="eyJhbGciOi..." class="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-xl text-white text-xs font-mono" required></textarea>
|
||||
<label class="block text-xs font-medium text-slate-700 mb-1">Access Token</label>
|
||||
<textarea name="token" rows="3" placeholder="eyJhbGciOi..." class="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 text-xs font-mono focus:bg-white focus:outline-none focus:ring-2 focus:ring-slate-900/10 focus:border-slate-900" required></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full py-3 bg-slate-800 hover:bg-slate-700 text-white font-bold rounded-xl text-xs uppercase tracking-wider transition border border-slate-700">
|
||||
<button type="submit" class="w-full py-2.5 bg-white hover:bg-slate-50 text-slate-800 font-medium rounded-xl text-xs transition border border-slate-200 cursor-pointer">
|
||||
Token Opslaan
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if($isAuthenticated)
|
||||
<div class="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||
<span class="text-xs text-emerald-400 font-bold flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||
<span>Momenteel actief gekoppeld</span>
|
||||
<div class="pt-3 border-t border-slate-100 flex items-center justify-between">
|
||||
<span class="text-xs text-emerald-600 font-medium flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
<span>Actief verbonden</span>
|
||||
</span>
|
||||
<form action="{{ route('admin.makerworld.disconnect') }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="text-xs text-rose-400 hover:underline font-bold">
|
||||
<button type="submit" class="text-xs text-rose-600 hover:underline font-medium cursor-pointer">
|
||||
Ontkoppelen
|
||||
</button>
|
||||
</form>
|
||||
@@ -596,7 +584,6 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -3,78 +3,69 @@
|
||||
@section('title', 'Artikel Beheer (Geïmporteerde Modellen) | Jouwdriedee.nl Beheer')
|
||||
|
||||
@section('content')
|
||||
<div class="py-8 bg-slate-950 text-slate-100 min-h-screen" x-data="artikelBeheerApp()">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
<div class="space-y-6" x-data="artikelBeheerApp()">
|
||||
|
||||
<!-- 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 class="flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse"></span>
|
||||
<span>Staging & Curation</span>
|
||||
<span>•</span>
|
||||
<span>Geïmporteerde Modellen</span>
|
||||
</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">
|
||||
<i data-lucide="layers" class="w-6 h-6"></i>
|
||||
</span>
|
||||
<span>Artikel Beheer</span>
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-slate-400">
|
||||
Beoordeel automatisch binnengehaalde MakerWorld modellen. Stel de gewenste categorie en verkoopprijs in en publiceer ze met 1 klik naar de webshop!
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">MakerWorld Modellen</span>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Artikel Beheer (Staging)</h1>
|
||||
<p class="mt-1 text-xs sm:text-sm text-slate-500">
|
||||
Beoordeel binnengehaalde modellen. Prijzen zijn automatisch berekend op basis van gewicht, printduur en kleuren via de 3D-calculator.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Header Quick Actions -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<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">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4 text-amber-400"></i>
|
||||
<span>Collecties & Auto-Sync</span>
|
||||
<div class="flex items-center space-x-2.5 self-start md:self-auto">
|
||||
<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">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<span>Collecties & Sync</span>
|
||||
</a>
|
||||
|
||||
<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">
|
||||
<i data-lucide="box" class="w-4 h-4"></i>
|
||||
<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>
|
||||
|
||||
<!-- Notification Alerts -->
|
||||
@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
|
||||
<!-- 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>
|
||||
|
||||
@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>
|
||||
<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>
|
||||
@endif
|
||||
|
||||
<!-- 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>
|
||||
<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, 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"
|
||||
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>
|
||||
|
||||
<!-- 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">
|
||||
<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">
|
||||
<option value="">Alle collecties</option>
|
||||
@foreach($collections as $col)
|
||||
<option value="{{ $col->id }}" {{ request('collection_id') == $col->id ? 'selected' : '' }}>
|
||||
@@ -84,54 +75,52 @@
|
||||
</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 type="submit" class="px-3 py-1.5 rounded-lg bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium transition">
|
||||
Filter
|
||||
</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 href="{{ route('admin.makerworld.staging') }}" class="text-xs text-rose-600 hover:underline">
|
||||
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 class="text-xs text-slate-500 font-medium">
|
||||
<span>{{ $totalStaged }} concepten wachten op review</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>
|
||||
<!-- 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-3">
|
||||
<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-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">
|
||||
<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-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 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-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 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>
|
||||
|
||||
@@ -140,7 +129,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -149,10 +138,10 @@
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
@@ -160,124 +149,140 @@
|
||||
|
||||
<!-- 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 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-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.
|
||||
<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-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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<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-slate-900 rounded-3xl border border-slate-800 overflow-hidden flex flex-col justify-between shadow-lg hover:border-slate-700 transition relative">
|
||||
<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">
|
||||
|
||||
<!-- Top Image Preview & Gallery -->
|
||||
<div>
|
||||
<div class="relative aspect-4/3 bg-slate-950 overflow-hidden group">
|
||||
<!-- Top Image Preview & Gallery -->
|
||||
<div class="relative aspect-4/3 bg-slate-50 overflow-hidden group">
|
||||
<img src="{{ $product->image_url }}" alt="{{ $product->name }}" class="w-full h-full object-cover">
|
||||
|
||||
<!-- Selection Checkbox -->
|
||||
<div class="absolute top-3 left-3 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">
|
||||
<input type="checkbox" value="{{ $product->id }}" x-model="selectedIds" class="w-4 h-4 text-amber-500 rounded focus:ring-0">
|
||||
<div class="absolute top-2.5 left-2.5 z-10">
|
||||
<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-3.5 h-3.5 text-slate-900 rounded focus:ring-0">
|
||||
</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">
|
||||
<div class="absolute top-2.5 right-2.5 flex flex-col items-end gap-1">
|
||||
<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">
|
||||
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]">
|
||||
<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]">
|
||||
{{ $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">
|
||||
<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">
|
||||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Photo Thumbnails Strip (All Pulled Pictures) -->
|
||||
<!-- Photo Thumbnails Strip -->
|
||||
@if(!empty($product->gallery_images) && count($product->gallery_images) > 0)
|
||||
<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="text-[10px] font-bold text-slate-500 uppercase tracking-wider shrink-0">Foto's:</span>
|
||||
<img src="{{ $product->image_url }}" alt="" class="w-9 h-9 rounded-lg object-cover border-2 border-amber-400 shrink-0">
|
||||
@foreach(array_slice($product->gallery_images, 0, 6) as $galleryPic)
|
||||
<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">
|
||||
<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) > 6)
|
||||
<span class="text-[10px] text-slate-400 font-bold shrink-0">+{{ count($product->gallery_images) - 6 }} meer</span>
|
||||
@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
|
||||
|
||||
<!-- 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">
|
||||
<!-- 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-[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 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-[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 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-[10px] font-bold text-amber-400 block uppercase">Adviesprijs</span>
|
||||
<span class="font-black text-amber-400">
|
||||
<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>
|
||||
|
||||
<!-- Meta Details -->
|
||||
<div class="p-4 space-y-2">
|
||||
<div class="text-[11px] text-slate-400 flex items-center justify-between">
|
||||
<span class="truncate">Maker: <strong class="text-slate-300">{{ $product->makerworld_author ?? 'MakerWorld Maker' }}</strong></span>
|
||||
<span class="font-mono text-slate-500 text-[10px]">#{{ $product->makerworld_id }}</span>
|
||||
<div class="p-3">
|
||||
<div class="text-[11px] text-slate-500 flex items-center justify-between">
|
||||
<span class="truncate">Maker: <strong class="text-slate-700">{{ $product->makerworld_author ?? 'MakerWorld' }}</strong></span>
|
||||
<span class="font-mono text-slate-400 text-[10px]">#{{ $product->makerworld_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
@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>
|
||||
<span class="font-bold text-slate-900">{{ $product->formatted_price }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<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">
|
||||
Bewerken
|
||||
</a>
|
||||
<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">
|
||||
<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
|
||||
|
||||
<!-- Editable Product Name -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Producttitel</label>
|
||||
<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-3 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-white text-xs font-bold focus:ring-amber-400"
|
||||
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">
|
||||
<!-- Category selector -->
|
||||
<div>
|
||||
<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">
|
||||
<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 }}
|
||||
@@ -286,39 +291,41 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Retail Price -->
|
||||
<div>
|
||||
<label class="block text-[10px] font-black uppercase tracking-wider text-slate-400 mb-1">Verkoopprijs (€)</label>
|
||||
<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.5 py-1.5 bg-slate-900 border border-slate-700 rounded-xl text-amber-300 text-xs font-black focus:ring-amber-400"
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
<div class="pt-1 flex items-center justify-between gap-2">
|
||||
<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">
|
||||
<i data-lucide="check" class="w-3.5 h-3.5"></i>
|
||||
<span>Publiceer</span>
|
||||
</button>
|
||||
|
||||
<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">
|
||||
<i data-lucide="sliders" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
|
||||
<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"
|
||||
class="p-1.5 rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 border border-slate-200 transition cursor-pointer"
|
||||
title="Verwijderen"
|
||||
>
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
<i data-lucide="trash-2" class="w-3.5 h-3.5"></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')
|
||||
@@ -327,13 +334,11 @@
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8">
|
||||
<div class="mt-6">
|
||||
{{ $stagedProducts->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -353,4 +358,3 @@ function artikelBeheerApp() {
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -6,65 +6,65 @@
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- 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 class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
|
||||
<i data-lucide="shopping-cart" class="w-4 h-4"></i>
|
||||
<span>Verkopen & Facturatie</span>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Verkopen & Orders</span>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-white">Bestellingen ({{ $orders->total() }})</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-400 mt-1">Inzicht in binnengekomen bestellingen, status en klantgegevens.</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Bestellingen ({{ $orders->total() }})</h1>
|
||||
<p class="text-xs text-slate-500 mt-1">Overzicht van binnengekomen bestellingen, orderstatussen en verzending.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<table class="w-full text-left text-xs text-slate-300">
|
||||
<thead class="bg-slate-950/80 text-[11px] uppercase tracking-wider text-slate-400 font-bold border-b border-slate-800">
|
||||
<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.5 px-4">Order #</th>
|
||||
<th class="py-3.5 px-4">Datum</th>
|
||||
<th class="py-3.5 px-4">Klant</th>
|
||||
<th class="py-3.5 px-4">Items</th>
|
||||
<th class="py-3.5 px-4">Totaal</th>
|
||||
<th class="py-3.5 px-4">Status</th>
|
||||
<th class="py-3.5 px-4 text-right">Details</th>
|
||||
<th class="py-3 px-4">Order #</th>
|
||||
<th class="py-3 px-4">Datum</th>
|
||||
<th class="py-3 px-4">Klant</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-800/80 font-medium">
|
||||
<tbody class="divide-y divide-slate-100 font-medium">
|
||||
@forelse($orders as $order)
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="py-3.5 px-4 font-mono font-bold text-white">
|
||||
<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-400">
|
||||
<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">
|
||||
<div class="font-bold text-white">{{ $order->customer_name }}</div>
|
||||
<div class="text-[11px] text-slate-400">{{ $order->customer_email }}</div>
|
||||
<div class="font-semibold text-slate-900">{{ $order->customer_name }}</div>
|
||||
<div class="text-[11px] text-slate-400 font-normal">{{ $order->customer_email }}</div>
|
||||
</td>
|
||||
<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
|
||||
</span>
|
||||
</td>
|
||||
<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 class="py-3.5 px-4">
|
||||
<span class="px-2.5 py-1 rounded-full text-[10px] font-black uppercase
|
||||
@if($order->status === 'completed' || $order->status === 'shipped') bg-emerald-500/20 text-emerald-400
|
||||
@elseif($order->status === 'printed' || $order->status === 'processing') bg-sky-500/20 text-sky-400
|
||||
@elseif($order->status === 'cancelled') bg-rose-500/20 text-rose-400
|
||||
@else bg-amber-500/20 text-amber-400 @endif
|
||||
">
|
||||
{{ $order->status }}
|
||||
<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="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>
|
||||
<i data-lucide="chevron-right" class="w-3 h-3"></i>
|
||||
</a>
|
||||
@@ -72,8 +72,8 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="p-10 text-center text-slate-500">
|
||||
Nog geen bestellingen binnengekomen.
|
||||
<td colspan="7" class="py-12 text-center text-slate-400">
|
||||
Nog geen bestellingen geplaatst.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
@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() }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -90,4 +90,3 @@
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
<div class="space-y-6 max-w-5xl mx-auto">
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
<span>Terug naar Bestellingen</span>
|
||||
</a>
|
||||
<h1 class="font-heading text-2xl font-black text-white">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>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-slate-900">Bestelling #{{ $order->order_number }}</h1>
|
||||
<p class="text-xs text-slate-500">Geplaatst op {{ $order->created_at->format('d-m-Y om H:i') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Status update form -->
|
||||
<form action="{{ route('admin.orders.status', $order) }}" method="POST" class="flex items-center space-x-2">
|
||||
@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="processing" {{ $order->status === 'processing' ? 'selected' : '' }}>In verwerking</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="cancelled" {{ $order->status === 'cancelled' ? 'selected' : '' }}>Geannuleerd</option>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
</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">
|
||||
|
||||
<!-- Order Items -->
|
||||
<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">
|
||||
<h3 class="font-heading font-black text-base text-white">Bestelde 3D Prints</h3>
|
||||
<div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4">
|
||||
<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)
|
||||
<div class="py-3.5 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h4 class="font-bold text-white text-sm">{{ $item->product_name }}</h4>
|
||||
<div class="text-xs text-slate-400 space-y-0.5 mt-1">
|
||||
<div><strong>Materiaal:</strong> {{ strtoupper($item->material) }}</div>
|
||||
<div><strong>Kleur:</strong> {{ $item->color }}</div>
|
||||
<div><strong>Schaal:</strong> {{ $item->scale ?? '100%' }}</div>
|
||||
<h4 class="font-semibold text-slate-900 text-xs">{{ $item->product_name }}</h4>
|
||||
<div class="text-[11px] text-slate-500 space-y-0.5 mt-1">
|
||||
<div><span class="text-slate-400">Materiaal:</span> {{ strtoupper($item->material) }}</div>
|
||||
<div><span class="text-slate-400">Kleur:</span> {{ $item->color }}</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 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 € {{ number_format($item->unit_price, 2, ',', '.') }}
|
||||
</span>
|
||||
<div class="text-xs font-black text-amber-400 mt-1">
|
||||
<div class="text-xs font-bold text-slate-900 mt-0.5">
|
||||
€ {{ number_format($item->total_price, 2, ',', '.') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,51 +78,53 @@
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-800 flex items-center justify-between font-heading font-black text-lg text-white">
|
||||
<span>Totaalbedrag</span>
|
||||
<span class="text-amber-400">{{ $order->formatted_total }}</span>
|
||||
<div class="pt-4 border-t border-slate-100 flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-slate-500">Totaalbedrag (incl. BTW)</span>
|
||||
<span class="font-bold text-base text-slate-900">{{ $order->formatted_total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Klantgegevens & Afleveradres -->
|
||||
<!-- Klant & Verzendinfo -->
|
||||
<div class="space-y-4">
|
||||
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-4 text-xs">
|
||||
<h3 class="font-heading font-black text-base text-white">Klant & Verzendadres</h3>
|
||||
|
||||
<div class="space-y-2 text-slate-300">
|
||||
<div>
|
||||
<span class="text-slate-500 block text-[10px] uppercase font-bold">Naam</span>
|
||||
<strong class="text-white">{{ $order->customer_name }}</strong>
|
||||
<div class="p-5 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-bold text-xs text-slate-900 uppercase tracking-wider">Klantgegevens</h3>
|
||||
<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">
|
||||
Klantprofiel →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="text-xs space-y-1.5 text-slate-600">
|
||||
<div>
|
||||
<span class="text-slate-500 block text-[10px] uppercase font-bold">E-mail</span>
|
||||
<span>{{ $order->customer_email }}</span>
|
||||
<span class="text-slate-400 block text-[10px]">Naam</span>
|
||||
<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>
|
||||
<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-500 block text-[10px] uppercase font-bold">Telefoon</span>
|
||||
<span>{{ $order->customer_phone }}</span>
|
||||
<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="pt-2 border-t border-slate-800">
|
||||
<span class="text-slate-500 block text-[10px] uppercase font-bold">Adres</span>
|
||||
<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>{{ $order->shipping_country ?? 'Nederland' }}</div>
|
||||
<div class="text-slate-400">Nederland</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($order->notes)
|
||||
<div class="pt-3 border-t border-slate-800">
|
||||
<span class="text-slate-500 block text-[10px] uppercase font-bold">Klant Opmerking</span>
|
||||
<p class="text-slate-300 italic mt-1">{{ $order->notes }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,86 @@
|
||||
@section('title', 'Nieuw Product Toevoegen | Jouwdriedee Beheer')
|
||||
|
||||
@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 -->
|
||||
<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>
|
||||
<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>
|
||||
<span>Terug naar Producten</span>
|
||||
</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>
|
||||
|
||||
@@ -20,17 +90,18 @@
|
||||
<form action="{{ route('admin.products.store') }}" method="POST" class="space-y-6">
|
||||
@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="md:col-span-2 space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<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-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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Categorie *</label>
|
||||
<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)
|
||||
<option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
@@ -40,55 +111,377 @@
|
||||
</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 • <span x-text="printTimeHours"></span>u • <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="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Basisprijs (€) *</label>
|
||||
<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 class="md:col-span-2 space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Afbeelding Pad of URL</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">
|
||||
<label class="block text-xs font-medium text-slate-700">Korte Omschrijving (voor catalogus cards)</label>
|
||||
<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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Korte Omschrijving (voor overzichtskaarten)</label>
|
||||
<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>
|
||||
<!-- Afbeeldingen & Galerij Beheer -->
|
||||
<div class="pt-5 border-t border-slate-200/60 space-y-4"
|
||||
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>
|
||||
|
||||
<!-- Beschrijving -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Volledige 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>
|
||||
<label class="block text-xs font-medium text-slate-700">Productbeschrijving *</label>
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<label class="block text-xs font-bold text-slate-300">Afmetingen (LxBxH)</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">
|
||||
<label class="block text-xs font-medium text-slate-700">Afmetingen</label>
|
||||
<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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Gewicht (gram)</label>
|
||||
<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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Printtijd (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">
|
||||
<label class="block text-xs font-medium text-slate-700">Printduur (uren)</label>
|
||||
<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>
|
||||
|
||||
<!-- Toggles -->
|
||||
<div class="pt-3 border-t border-slate-800 flex items-center space-x-6">
|
||||
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200">
|
||||
<input type="checkbox" name="is_active" value="1" checked class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700">
|
||||
<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">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200">
|
||||
<input type="checkbox" name="is_featured" value="1" class="w-4 h-4 rounded text-amber-400 bg-slate-950 border-slate-700">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
@@ -96,16 +489,15 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
</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>
|
||||
<span>Product Publiceren</span>
|
||||
<span>Product Opslaan</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,40 +3,113 @@
|
||||
@section('title', 'Product Bewerken: ' . $product->name . ' | Jouwdriedee Beheer')
|
||||
|
||||
@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 -->
|
||||
<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>
|
||||
<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>
|
||||
<span>Terug naar Producten</span>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<span>Bekijk Live</span>
|
||||
<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 text-slate-400"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<form action="{{ route('admin.products.update', $product) }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
@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="md:col-span-2 space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<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-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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Categorie *</label>
|
||||
<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)
|
||||
<option value="{{ $cat->id }}" {{ old('category_id', $product->category_id) == $cat->id ? 'selected' : '' }}>
|
||||
{{ $cat->name }}
|
||||
@@ -46,55 +119,378 @@
|
||||
</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 • <span x-text="printTimeHours"></span>u • <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="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Basisprijs (€) *</label>
|
||||
<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 class="md:col-span-2 space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Afbeelding Pad / URL</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">
|
||||
<label class="block text-xs font-medium text-slate-700">Korte Omschrijving (voor catalogus cards)</label>
|
||||
<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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Korte Omschrijving (voor catalogus cards)</label>
|
||||
<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>
|
||||
<!-- Visueel Afbeeldingen & Fotogalerij Beheer -->
|
||||
<div class="pt-5 border-t border-slate-200/60 space-y-4"
|
||||
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>
|
||||
|
||||
<!-- Beschrijving -->
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Volledige 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>
|
||||
<label class="block text-xs font-medium text-slate-700">Productbeschrijving *</label>
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<label class="block text-xs font-bold text-slate-300">Afmetingen (LxBxH)</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">
|
||||
<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-50/70 focus:bg-white border border-slate-200 rounded-lg px-3 py-1.5 text-xs text-slate-900">
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">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">
|
||||
<label class="block text-xs font-medium text-slate-700">Gewicht (gram)</label>
|
||||
<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 class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Printtijd (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">
|
||||
<label class="block text-xs font-medium text-slate-700">Printduur (uren)</label>
|
||||
<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>
|
||||
|
||||
<!-- Toggles -->
|
||||
<div class="pt-3 border-t border-slate-800 flex items-center space-x-6">
|
||||
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-2 cursor-pointer text-xs font-bold text-slate-200">
|
||||
<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">
|
||||
<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-slate-900 border-slate-300">
|
||||
<span>Uitlichten op homepage</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -102,10 +498,10 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
</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>
|
||||
<span>Wijzigingen Opslaan</span>
|
||||
</button>
|
||||
@@ -114,4 +510,3 @@
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -6,142 +6,183 @@
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- 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 class="inline-flex items-center space-x-2 text-xs text-amber-400 font-bold uppercase tracking-wider mb-1">
|
||||
<i data-lucide="box" class="w-4 h-4"></i>
|
||||
<span>Assortiment & Voorraad</span>
|
||||
</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>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400 block mb-1">Catalogus & Assortiment</span>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-bold text-slate-900 tracking-tight">Producten ({{ $products->total() }})</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-500 mt-1">Beheer prijzen, teksten, kostprijscalculaties en afbeeldingen van alle 3D prints.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3 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">
|
||||
<i data-lucide="download-cloud" class="w-4 h-4"></i>
|
||||
<span>MakerWorld Importer</span>
|
||||
<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-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-3.5 h-3.5 text-slate-400"></i>
|
||||
<span>MakerWorld Sync</span>
|
||||
</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">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Nieuw Product</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.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</span>
|
||||
<span class="text-[11px] text-slate-400">({{ $totalAll ?? $products->total() }})</span>
|
||||
</a>
|
||||
|
||||
<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-500"></span>
|
||||
<span>Live in Webshop</span>
|
||||
<span class="text-[11px] text-slate-400">({{ $totalPublished ?? 0 }})</span>
|
||||
</a>
|
||||
|
||||
<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' }}">
|
||||
<span>Concepten / Staging</span>
|
||||
@if(($totalDraft ?? 0) > 0)
|
||||
<span class="px-1.5 py-0.2 rounded-full text-[10px] font-semibold bg-amber-100 text-amber-800">
|
||||
{{ $totalDraft }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<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' }}">
|
||||
<span>MakerWorld</span>
|
||||
<span class="text-[11px] text-slate-400">({{ $totalMakerWorld ?? 0 }})</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<div class="relative flex-1 max-w-md">
|
||||
<div class="relative flex-1 max-w-sm">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ request('q') }}"
|
||||
placeholder="Zoek op naam of omschrijving..."
|
||||
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"
|
||||
placeholder="Zoek op naam..."
|
||||
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>
|
||||
|
||||
<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>
|
||||
@foreach($categories as $cat)
|
||||
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>{{ $cat->name }}</option>
|
||||
@endforeach
|
||||
</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
|
||||
</button>
|
||||
@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
|
||||
</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>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div class="rounded-3xl bg-slate-900 border border-slate-800 overflow-hidden shadow-xl">
|
||||
<!-- Products Table (Clean & Airy) -->
|
||||
<div class="rounded-xl bg-white border border-slate-200/60 overflow-hidden shadow-2xs">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs text-slate-300">
|
||||
<thead class="bg-slate-950/80 text-[11px] uppercase tracking-wider text-slate-400 font-bold border-b border-slate-800">
|
||||
<table class="w-full text-left text-xs text-slate-600">
|
||||
<thead class="bg-slate-50/70 text-[11px] uppercase tracking-wider text-slate-400 font-semibold border-b border-slate-200/60">
|
||||
<tr>
|
||||
<th class="py-3.5 px-4">Product</th>
|
||||
<th class="py-3.5 px-4">Categorie</th>
|
||||
<th class="py-3.5 px-4">Basisprijs</th>
|
||||
<th class="py-3.5 px-4">Specificaties</th>
|
||||
<th class="py-3.5 px-4">Status</th>
|
||||
<th class="py-3.5 px-4 text-right">Acties</th>
|
||||
<th class="py-3 px-4">Product</th>
|
||||
<th class="py-3 px-4">Categorie</th>
|
||||
<th class="py-3 px-4">Prijs</th>
|
||||
<th class="py-3 px-4">Specificaties</th>
|
||||
<th class="py-3 px-4">Status</th>
|
||||
<th class="py-3 px-4 text-right">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/80 font-medium">
|
||||
<tbody class="divide-y divide-slate-100 font-normal">
|
||||
@forelse($products as $product)
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="py-3.5 px-4">
|
||||
<tr class="hover:bg-slate-50/50 transition">
|
||||
<td class="py-3 px-4">
|
||||
<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">
|
||||
@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>
|
||||
<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 }}
|
||||
</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>
|
||||
</td>
|
||||
|
||||
<td class="py-3.5 px-4">
|
||||
<td class="py-3 px-4">
|
||||
@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 }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-slate-500">-</span>
|
||||
<span class="text-slate-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
<td class="py-3.5 px-4">
|
||||
<span class="font-black text-amber-400 font-heading text-sm">{{ $product->formatted_price }}</span>
|
||||
<td class="py-3 px-4">
|
||||
<span class="font-bold text-slate-900 font-heading text-xs">{{ $product->formatted_price }}</span>
|
||||
</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->weight_grams ?? 0 }}g • {{ $product->print_time_hours ?? 0 }}u</div>
|
||||
</td>
|
||||
|
||||
<td class="py-3.5 px-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($product->is_active)
|
||||
<span class="inline-flex items-center space-x-1 text-emerald-400 text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
|
||||
<span>Actief</span>
|
||||
<td class="py-3 px-4">
|
||||
<div class="flex flex-col gap-1 items-start">
|
||||
@if($product->status === 'draft')
|
||||
<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">
|
||||
<span>Concept</span>
|
||||
</span>
|
||||
@elseif($product->is_active)
|
||||
<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-500"></span>
|
||||
<span>Live</span>
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center space-x-1 text-slate-500 text-[11px] font-bold">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-slate-500"></span>
|
||||
<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-300"></span>
|
||||
<span>Verborgen</span>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($product->is_featured)
|
||||
<span class="inline-flex items-center text-amber-400 text-[10px] font-bold">
|
||||
★ Uitgelicht
|
||||
@if($product->makerworld_id)
|
||||
<span class="text-[10px] text-slate-400 font-mono">
|
||||
MW #{{ $product->makerworld_id }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="py-3.5 px-4 text-right">
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
<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">
|
||||
<td class="py-3 px-4 text-right">
|
||||
<div class="flex items-center justify-end space-x-1">
|
||||
<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>
|
||||
</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>
|
||||
</a>
|
||||
<form action="{{ route('admin.products.destroy', $product) }}" method="POST" onsubmit="return confirm('Weet je zeker dat je dit product wilt verwijderen?')">
|
||||
@csrf
|
||||
@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>
|
||||
</button>
|
||||
</form>
|
||||
@@ -150,7 +191,7 @@
|
||||
</tr>
|
||||
@empty
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -160,7 +201,7 @@
|
||||
</div>
|
||||
|
||||
@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() }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -168,4 +209,3 @@
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,26 +3,23 @@
|
||||
@section('title', 'Wachtwoord Vergeten | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
<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-sm space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-2">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-indigo-100 text-indigo-900 shadow-2xs mb-1">
|
||||
<i data-lucide="key-round" class="w-6 h-6 text-indigo-600"></i>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Wachtwoord vergeten</h1>
|
||||
<p class="text-xs text-slate-500">Vul je e-mailadres in en ontvang een beveiligde herstellink</p>
|
||||
<div class="text-center space-y-1.5">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">Wachtwoord vergeten</h1>
|
||||
<p class="text-xs text-slate-500">Vul je e-mailadres in om een herstellink te ontvangen</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
|
||||
<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">
|
||||
@csrf
|
||||
|
||||
<!-- E-mailadres -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="email" class="block text-xs font-bold text-slate-700">Jouw geregistreerde e-mailadres</label>
|
||||
<div class="space-y-1">
|
||||
<label for="email" class="block text-xs font-medium text-slate-700">Jouw e-mailadres</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="email"
|
||||
@@ -32,26 +29,26 @@
|
||||
required
|
||||
autofocus
|
||||
placeholder="jouw@email.nl"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
|
||||
<i data-lucide="send" class="w-4 h-4"></i>
|
||||
<span>Herstellink Versturen</span>
|
||||
<div class="pt-1">
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Herstellink versturen</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600">
|
||||
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline inline-flex items-center space-x-1">
|
||||
<div class="pt-3 border-t border-slate-100 text-center text-xs text-slate-500">
|
||||
<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>
|
||||
<span>Terug naar inloggen</span>
|
||||
</a>
|
||||
@@ -61,4 +58,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,26 +3,23 @@
|
||||
@section('title', 'Inloggen | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
<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-sm space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-2">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1">
|
||||
<i data-lucide="log-in" class="w-6 h-6 text-amber-600"></i>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Welkom terug</h1>
|
||||
<div class="text-center space-y-1.5">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">Welkom terug</h1>
|
||||
<p class="text-xs text-slate-500">Log in met jouw e-mailadres en wachtwoord</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
|
||||
<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">
|
||||
@csrf
|
||||
|
||||
<!-- E-mailadres -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label>
|
||||
<div class="space-y-1">
|
||||
<label for="email" class="block text-xs font-medium text-slate-700">E-mailadres</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="email"
|
||||
@@ -32,20 +29,20 @@
|
||||
required
|
||||
autofocus
|
||||
placeholder="jouw@email.nl"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- Wachtwoord -->
|
||||
<div class="space-y-1.5">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord</label>
|
||||
<a href="{{ route('password.request') }}" class="text-[11px] font-bold text-rose-600 hover:text-rose-700 transition">
|
||||
<label for="password" class="block text-xs font-medium text-slate-700">Wachtwoord</label>
|
||||
<a href="{{ route('password.request') }}" class="text-[11px] font-medium text-slate-500 hover:text-slate-900 transition">
|
||||
Wachtwoord vergeten?
|
||||
</a>
|
||||
</div>
|
||||
@@ -56,35 +53,35 @@
|
||||
id="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- Onthoud mij -->
|
||||
<div class="flex items-center justify-between pt-1">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="checkbox" name="remember" value="1" class="w-4 h-4 rounded text-amber-500 focus:ring-amber-400 border-slate-300">
|
||||
<span class="text-xs font-bold text-slate-600">Onthoud mij op dit apparaat</span>
|
||||
<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 text-slate-600">Onthoud mij op dit apparaat</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
|
||||
<div class="pt-1">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -93,4 +90,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,26 +3,23 @@
|
||||
@section('title', 'Registreren | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-[75vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
<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-sm space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-2">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-rose-100 text-rose-900 shadow-2xs mb-1">
|
||||
<i data-lucide="user-plus" class="w-6 h-6 text-rose-600"></i>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Account aanmaken</h1>
|
||||
<p class="text-xs text-slate-500">Meld je aan om bestellingen te volgen en sneller te bestellen</p>
|
||||
<div class="text-center space-y-1.5">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">Account aanmaken</h1>
|
||||
<p class="text-xs text-slate-500">Meld je aan om bestellingen te volgen en sneller af te rekenen</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
|
||||
<form action="{{ route('register.submit') }}" method="POST" class="space-y-4">
|
||||
<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-3.5">
|
||||
@csrf
|
||||
|
||||
<!-- Naam -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="name" class="block text-xs font-bold text-slate-700">Voor- en achternaam *</label>
|
||||
<div class="space-y-1">
|
||||
<label for="name" class="block text-xs font-medium text-slate-700">Voor- en achternaam *</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
@@ -31,19 +28,19 @@
|
||||
value="{{ old('name') }}"
|
||||
required
|
||||
autofocus
|
||||
placeholder="bijv. Jan Jansen"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('name') border-rose-400 @enderror"
|
||||
placeholder="Jan Jansen"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- E-mailadres -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres *</label>
|
||||
<div class="space-y-1">
|
||||
<label for="email" class="block text-xs font-medium text-slate-700">E-mailadres *</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="email"
|
||||
@@ -52,18 +49,18 @@
|
||||
value="{{ old('email') }}"
|
||||
required
|
||||
placeholder="jouw@email.nl"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('email') border-rose-400 @enderror"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- Wachtwoord -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="password" class="block text-xs font-bold text-slate-700">Wachtwoord (minimaal 8 tekens) *</label>
|
||||
<div class="space-y-1">
|
||||
<label for="password" class="block text-xs font-medium text-slate-700">Wachtwoord (minimaal 8 tekens) *</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
@@ -71,18 +68,18 @@
|
||||
id="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
|
||||
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>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- Bevestig Wachtwoord -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig wachtwoord *</label>
|
||||
<div class="space-y-1">
|
||||
<label for="password_confirmation" class="block text-xs font-medium text-slate-700">Bevestig wachtwoord *</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="password"
|
||||
@@ -90,25 +87,25 @@
|
||||
id="password_confirmation"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl pl-9 pr-3.5 py-2.5 text-xs text-slate-900 font-semibold placeholder-slate-400 focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
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>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
|
||||
<span>Account Aanmaken</span>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||
<button 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 registreren</span>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pt-4 border-t border-slate-100 text-center text-xs font-bold text-slate-600">
|
||||
<span>Heb je al een account?</span>
|
||||
<a href="{{ route('login') }}" class="text-rose-600 hover:text-rose-700 underline ml-1">
|
||||
Hier inloggen
|
||||
<div class="pt-3 border-t border-slate-100 text-center text-xs text-slate-500">
|
||||
<span>Al een account?</span>
|
||||
<a href="{{ route('login') }}" class="text-slate-900 font-semibold hover:underline ml-1">
|
||||
Inloggen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,4 +113,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,27 +3,24 @@
|
||||
@section('title', 'Nieuw Wachtwoord Instellen | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<div class="min-h-[70vh] flex items-center justify-center py-10 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
<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-sm space-y-6">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center space-y-2">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-amber-100 text-amber-900 shadow-2xs mb-1">
|
||||
<i data-lucide="lock" class="w-6 h-6 text-amber-600"></i>
|
||||
</div>
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">Nieuw wachtwoord</h1>
|
||||
<p class="text-xs text-slate-500">Kies een nieuw en veilig wachtwoord voor jouw account</p>
|
||||
<div class="text-center space-y-1.5">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">Nieuw wachtwoord</h1>
|
||||
<p class="text-xs text-slate-500">Kies een nieuw en veilig wachtwoord voor je account</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Card -->
|
||||
<div class="p-6 sm:p-8 rounded-3xl bg-white border border-slate-200/80 shadow-lg space-y-5">
|
||||
<form action="{{ route('password.update') }}" method="POST" class="space-y-4">
|
||||
<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-3.5">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
|
||||
<!-- E-mailadres -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="email" class="block text-xs font-bold text-slate-700">E-mailadres</label>
|
||||
<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"
|
||||
@@ -31,16 +28,16 @@
|
||||
value="{{ old('email', $email) }}"
|
||||
required
|
||||
readonly
|
||||
class="w-full bg-slate-100 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-600 font-semibold cursor-not-allowed"
|
||||
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')
|
||||
<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
|
||||
</div>
|
||||
|
||||
<!-- Nieuw Wachtwoord -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="password" class="block text-xs font-bold text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
|
||||
<div class="space-y-1">
|
||||
<label for="password" class="block text-xs font-medium text-slate-700">Nieuw wachtwoord (min. 8 tekens)</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
@@ -48,31 +45,30 @@
|
||||
required
|
||||
autofocus
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400 @error('password') border-rose-400 @enderror"
|
||||
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')
|
||||
<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
|
||||
</div>
|
||||
|
||||
<!-- Bevestig Nieuw Wachtwoord -->
|
||||
<div class="space-y-1.5">
|
||||
<label for="password_confirmation" class="block text-xs font-bold text-slate-700">Bevestig nieuw wachtwoord</label>
|
||||
<div class="space-y-1">
|
||||
<label for="password_confirmation" class="block text-xs font-medium text-slate-700">Bevestig nieuw wachtwoord</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
id="password_confirmation"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="w-full bg-slate-50 border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs text-slate-900 font-semibold focus:bg-white focus:outline-none focus:ring-2 focus:ring-amber-400"
|
||||
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>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="w-full py-3 px-5 rounded-2xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-black text-xs sm:text-sm transition flex items-center justify-center space-x-2 shadow-xs cursor-pointer">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
<span>Wachtwoord Opslaan & Inloggen</span>
|
||||
<button 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>Wachtwoord Opslaan</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -81,4 +77,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Winkelwagen 🛒 | Jouwdriedee.nl')
|
||||
@section('title', 'Winkelwagen | Jouwdriedee.nl')
|
||||
|
||||
@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">
|
||||
<h1 class="font-heading text-2xl sm:text-3xl font-black text-slate-900 tracking-tight flex items-center space-x-2">
|
||||
<span>🛒</span>
|
||||
<span>Jouw Winkelwagen</span>
|
||||
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900">
|
||||
Winkelwagen
|
||||
</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 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))
|
||||
<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="w-16 h-16 rounded-2xl bg-amber-50 text-slate-800 flex items-center justify-center mx-auto text-3xl">
|
||||
🧸
|
||||
<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-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>
|
||||
<h2 class="font-heading text-xl font-black text-slate-900">Je winkelmandje 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>
|
||||
<div class="pt-2 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">
|
||||
<span>Bekijk Alle Prints 🎁</span>
|
||||
<h2 class="text-base font-bold text-slate-900">Je winkelwagen is nog leeg</h2>
|
||||
<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-3 flex flex-col sm:flex-row items-center justify-center gap-2">
|
||||
<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 collectie</span>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
</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">
|
||||
<span>Eigen Model Bestellen 🚀</span>
|
||||
<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 bestand uploaden</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@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 -->
|
||||
<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)
|
||||
<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="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">
|
||||
</div>
|
||||
<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">
|
||||
<span class="px-2 py-0.2 rounded-md bg-rose-50 text-rose-700 font-bold">{{ $item['material'] }}</span>
|
||||
<span>•</span>
|
||||
<span class="font-semibold text-slate-700">Kleur: {{ $item['color'] }}</span>
|
||||
<span>•</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>Kleur: {{ $item['color'] }}</span>
|
||||
<span>•</span>
|
||||
<span class="font-mono text-slate-600">{{ $item['scale'] }}</span>
|
||||
</div>
|
||||
@if(!empty($item['custom_file_name']))
|
||||
<div class="text-[10px] font-bold text-indigo-600 flex items-center space-x-1 pt-0.5">
|
||||
<span>📁</span>
|
||||
<span>Bestand: {{ $item['custom_file_name'] }}</span>
|
||||
<div class="text-[10px] text-slate-500 font-mono flex items-center space-x-1 pt-0.5">
|
||||
<i data-lucide="file-code" class="w-3 h-3 text-slate-400"></i>
|
||||
<span>{{ $item['custom_file_name'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($item['custom_model_url']))
|
||||
<div class="text-[10px] font-bold text-indigo-600 flex items-center space-x-1 pt-0.5">
|
||||
<span>🔗</span>
|
||||
<div class="text-[10px] text-slate-500 font-mono flex items-center space-x-1 pt-0.5">
|
||||
<i data-lucide="link" class="w-3 h-3 text-slate-400"></i>
|
||||
<span class="truncate max-w-xs">{{ $item['custom_model_url'] }}</span>
|
||||
</div>
|
||||
@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
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
<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
|
||||
<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">
|
||||
<button type="submit" class="p-1 rounded-lg text-slate-400 hover:text-slate-800 transition" title="Bijwerken">
|
||||
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
|
||||
<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 cursor-pointer" title="Bijwerken">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</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, ',', '.') }}
|
||||
</span>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<form action="{{ route('cart.remove', $key) }}" method="POST">
|
||||
@csrf
|
||||
@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>
|
||||
</button>
|
||||
</form>
|
||||
@@ -99,45 +99,45 @@
|
||||
|
||||
<!-- Samenvatting -->
|
||||
<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">
|
||||
<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">
|
||||
<span>🧾</span>
|
||||
<span>Overzicht Bestelling</span>
|
||||
<div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4 sticky top-24">
|
||||
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-2 border-b border-slate-100">
|
||||
Besteloverzicht
|
||||
</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">
|
||||
<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 class="flex justify-between text-slate-600">
|
||||
<span>Verzendkosten</span>
|
||||
@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
|
||||
<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
|
||||
</div>
|
||||
@if($subtotal < 50)
|
||||
<p class="text-[11px] text-amber-900 bg-amber-50 p-2.5 rounded-xl border border-amber-200 font-bold">
|
||||
💡 Tip: Bestel nog voor <strong>€ {{ number_format(50 - $subtotal, 2, ',', '.') }}</strong> voor gratis verzending!
|
||||
<p class="text-[11px] text-slate-600 bg-slate-50 p-2.5 rounded-xl border border-slate-200/60">
|
||||
Bestel nog voor <strong>€ {{ number_format(50 - $subtotal, 2, ',', '.') }}</strong> voor gratis verzending.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<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="font-heading text-2xl font-black text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span>
|
||||
<span class="text-xs font-medium text-slate-600">Totaal (incl. btw)</span>
|
||||
<span class="text-xl font-bold text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span>
|
||||
</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">
|
||||
<span>Doorgaan Naar Afrekenen 🚀</span>
|
||||
<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>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
|
||||
<div class="flex items-center justify-center space-x-2 text-[11px] font-bold text-slate-400 pt-1">
|
||||
<span>🔒 Veilig betalen met iDEAL</span>
|
||||
<span>•</span>
|
||||
<span>PostNL</span>
|
||||
<div class="flex items-center justify-center space-x-2 text-[11px] text-slate-400 pt-1">
|
||||
<span>Veilig betalen via iDEAL</span>
|
||||
<span>•</span>
|
||||
<span>PostNL bezorging</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
@section('title', 'Afrekenen | Jouwdriedee.nl')
|
||||
|
||||
@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">
|
||||
<h1 class="text-2xl sm:text-3xl font-black text-slate-900 tracking-tight">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>
|
||||
<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-1">Vul je bezorgadres in om jouw 3D prints in de productiewachtrij te plaatsen.</p>
|
||||
</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">
|
||||
@csrf
|
||||
<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">
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<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 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>
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<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 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>
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">Telefoonnummer (voor bezorgupdates)</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">
|
||||
<div class="space-y-1">
|
||||
<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 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>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3.5">
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Straatnaam *</label>
|
||||
<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>
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<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 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>
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<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="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 class="sm:col-span-2">
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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">
|
||||
<div class="sm:col-span-2 space-y-1">
|
||||
<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 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>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
</h2>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-indigo-200 bg-indigo-50/40 cursor-pointer">
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="radio" name="payment_method" value="ideal" checked class="text-indigo-600 focus:ring-indigo-500">
|
||||
<span class="text-xs font-bold text-slate-900">iDEAL</span>
|
||||
<div class="space-y-2">
|
||||
<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-2.5">
|
||||
<input type="radio" name="payment_method" value="ideal" checked class="text-slate-900 focus:ring-slate-900">
|
||||
<span class="text-xs font-semibold text-slate-900">iDEAL</span>
|
||||
</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 class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer">
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="radio" name="payment_method" value="bancontact" class="text-indigo-600 focus:ring-indigo-500">
|
||||
<span class="text-xs font-bold text-slate-900">Bancontact</span>
|
||||
<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-2.5">
|
||||
<input type="radio" name="payment_method" value="bancontact" class="text-slate-900 focus:ring-slate-900">
|
||||
<span class="text-xs font-semibold text-slate-900">Bancontact</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-slate-400">België</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center justify-between p-3.5 rounded-2xl border border-slate-200 hover:bg-slate-50 cursor-pointer">
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="radio" name="payment_method" value="overboeking" class="text-indigo-600 focus:ring-indigo-500">
|
||||
<span class="text-xs font-bold text-slate-900">Handmatige Bankoverschrijving</span>
|
||||
<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-2.5">
|
||||
<input type="radio" name="payment_method" value="overboeking" class="text-slate-900 focus:ring-slate-900">
|
||||
<span class="text-xs font-semibold text-slate-900">Handmatige Bankoverschrijving</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-slate-400">Achteraf / Factuur</span>
|
||||
<span class="text-[11px] text-slate-400">Achteraf</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-700 mb-1">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>
|
||||
<div class="space-y-1 pt-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Opmerkingen voor de printer (optioneel)</label>
|
||||
<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>
|
||||
|
||||
@@ -116,49 +116,49 @@
|
||||
|
||||
<!-- Rechterkant: Besteloverzicht Card -->
|
||||
<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">
|
||||
<h3 class="text-sm font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">Jouw Bestelling</h3>
|
||||
<div class="p-6 rounded-2xl bg-white border border-slate-200/80 shadow-xs space-y-4 sticky top-24">
|
||||
<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)
|
||||
<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="font-bold text-slate-900">{{ $item['name'] }} <span class="text-slate-400 font-normal">× {{ $item['quantity'] }}</span></div>
|
||||
<div class="text-[11px] text-slate-500">{{ $item['material'] }} • {{ $item['color'] }} ({{ $item['scale'] }})</div>
|
||||
<div class="font-medium text-slate-900">{{ $item['name'] }} <span class="text-slate-400">× {{ $item['quantity'] }}</span></div>
|
||||
<div class="text-[11px] text-slate-500">{{ strtoupper($item['material']) }} • {{ $item['color'] }} ({{ $item['scale'] }})</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, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</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">
|
||||
<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 class="flex justify-between text-slate-600">
|
||||
<span>Verzendkosten</span>
|
||||
@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
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-100 flex justify-between items-baseline">
|
||||
<span class="text-sm font-bold text-slate-900">Totaalbedrag</span>
|
||||
<span class="text-2xl font-black text-indigo-600 font-mono">€ {{ number_format($total, 2, ',', '.') }}</span>
|
||||
<div class="pt-3 border-t border-slate-100 flex justify-between items-baseline">
|
||||
<span class="text-xs font-medium text-slate-600">Totaalbedrag</span>
|
||||
<span class="text-xl font-bold text-slate-900">€ {{ number_format($total, 2, ',', '.') }}</span>
|
||||
</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">
|
||||
<i data-lucide="check-circle-2" class="w-4 h-4"></i>
|
||||
<span>Bestelling Definitief Plaatsen</span>
|
||||
<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" class="w-4 h-4"></i>
|
||||
<span>Bestelling Plaatsen</span>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,4 +168,3 @@
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -1,105 +1,98 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Bestelling Ontvangen! | Jouwdriedee.nl')
|
||||
@section('title', 'Bestelling Ontvangen | Jouwdriedee.nl')
|
||||
|
||||
@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 -->
|
||||
<div class="space-y-4">
|
||||
<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">
|
||||
<i data-lucide="check" class="w-10 h-10 stroke-[2.5]"></i>
|
||||
<div class="space-y-3">
|
||||
<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-7 h-7 stroke-[2]"></i>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-black text-slate-900 tracking-tight">Bedankt voor je bestelling!</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-500 max-w-md mx-auto">
|
||||
We hebben jouw bestelling ontvangen. De printbestanden worden klaargezet voor de 3D-printers.
|
||||
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight text-slate-900">Bedankt voor je bestelling!</h1>
|
||||
<p class="text-xs text-slate-500 max-w-sm mx-auto">
|
||||
We hebben jouw bestelling ontvangen. De printbestanden worden klaargezet voor productie.
|
||||
</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">
|
||||
Bestelnummer: <span class="text-indigo-600">{{ $order->order_number }}</span>
|
||||
<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="font-bold text-slate-900">{{ $order->order_number }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<i data-lucide="printer" class="w-4 h-4 text-indigo-600"></i>
|
||||
<span>Live Print & Productie Status</span>
|
||||
<i data-lucide="printer" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>Productiestatus</span>
|
||||
</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 -->
|
||||
<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>
|
||||
<h4 class="text-xs font-bold text-slate-900">1. Bestelling ontvangen & geverifieerd</h4>
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
<!-- Stap 2: Slicing & Printbed -->
|
||||
<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>
|
||||
<h4 class="text-xs font-bold text-indigo-600">2. Slicing & printwachtrij (Huidige fase)</h4>
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
<!-- Stap 3: 3D Printen -->
|
||||
<div class="relative opacity-60">
|
||||
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span>
|
||||
<h4 class="text-xs font-bold text-slate-700">3. 3D Printen in de studio</h4>
|
||||
<div class="relative opacity-50">
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
<!-- Stap 4: Kwaliteitscontrole & Verzenden -->
|
||||
<div class="relative opacity-60">
|
||||
<span class="absolute -left-[31px] top-0.5 w-4 h-4 rounded-full bg-slate-300"></span>
|
||||
<h4 class="text-xs font-bold text-slate-700">4. Kwaliteitscontrole, inpakken & PostNL verzending</h4>
|
||||
<p class="text-[11px] text-slate-500 mt-0.5">Je ontvangt per e-mail direct een Track & Trace code.</p>
|
||||
<div class="relative opacity-50">
|
||||
<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-medium text-slate-700">4. Kwaliteitscontrole & PostNL verzending</h4>
|
||||
<p class="text-[11px] text-slate-500 mt-0.5">Je ontvangt per e-mail een Track & Trace code.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Besteloverzicht Card -->
|
||||
<div class="p-6 rounded-3xl bg-white border border-slate-200 shadow-sm text-left space-y-4">
|
||||
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider pb-3 border-b border-slate-100">
|
||||
<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-2 border-b border-slate-100">
|
||||
Overzicht van jouw bestelling
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
@foreach($order->items as $item)
|
||||
<div class="flex items-center justify-between text-xs py-1">
|
||||
<div>
|
||||
<span class="font-bold text-slate-900">{{ $item->product_name }}</span>
|
||||
<span class="text-slate-400">× {{ $item->quantity }}</span>
|
||||
<div class="text-[11px] text-slate-500">{{ $item->material }} • {{ $item->color }} ({{ $item->scale }})</div>
|
||||
<div class="font-medium text-slate-900">{{ $item->product_name }} <span class="text-slate-400">× {{ $item->quantity }}</span></div>
|
||||
<div class="text-[11px] text-slate-500">{{ strtoupper($item->material) }} • {{ $item->color }} ({{ $item->scale }})</div>
|
||||
</div>
|
||||
<span class="font-mono font-bold text-slate-900">
|
||||
€ {{ number_format($item->total_price, 2, ',', '.') }}
|
||||
<span class="font-semibold text-slate-900">
|
||||
€ {{ number_format($item->total_price, 2, ',', '.') }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<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="text-lg font-black text-indigo-600 font-mono">{{ $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 }}
|
||||
<span class="font-medium text-slate-500">Totaal betaald</span>
|
||||
<span class="text-base font-bold text-slate-900">{{ $order->formatted_total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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">
|
||||
<span>Terug Naar De Homepagina</span>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<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">
|
||||
Terug naar de homepagina
|
||||
</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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -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 & 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 & Levering</span>
|
||||
<span class="font-semibold text-slate-900">PostNL Track & 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 & 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 & 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 & 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 & Beantwoord in Admin →
|
||||
</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
|
||||
|
||||
@@ -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>
|
||||
© {{ date('Y') }} Jouwdriedee.nl • 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>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,88 +1,248 @@
|
||||
<!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>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>@yield('title', 'Admin Dashboard | Jouwdriedee.nl')</title>
|
||||
<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.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'])
|
||||
|
||||
<style>
|
||||
body { font-family: 'Nunito', system-ui, sans-serif; }
|
||||
h1, h2, h3, h4, .font-heading { font-family: 'Fredoka', ui-rounded, 'Nunito', sans-serif; }
|
||||
body {
|
||||
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>
|
||||
</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 -->
|
||||
<header class="bg-slate-900/90 backdrop-blur-md border-b border-slate-800 sticky top-0 z-30">
|
||||
<!-- Admin Header (Sleek, Minimalist, SaaS Style) -->
|
||||
<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="flex items-center justify-between h-16">
|
||||
<div class="flex items-center justify-between h-16 gap-4">
|
||||
|
||||
<!-- Brand / Logo -->
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="{{ route('admin.content.index') }}" class="flex items-center space-x-3 group">
|
||||
<img src="/images/logo.svg" alt="Jouwdriedee.nl" class="h-8 w-auto brightness-200 invert">
|
||||
<span class="px-2 py-0.5 rounded-md bg-amber-400 text-slate-950 font-black text-[10px] uppercase tracking-wider">Beheer</span>
|
||||
<!-- Brand / Logo (Always strictly shrink-0 so it never overlaps or wraps) -->
|
||||
<div class="flex items-center space-x-3 shrink-0">
|
||||
<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-6 w-auto shrink-0">
|
||||
<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 Nav Links -->
|
||||
<nav class="hidden md:flex items-center space-x-1 text-xs font-bold">
|
||||
<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' }}">
|
||||
<i data-lucide="palette" class="w-4 h-4"></i>
|
||||
<!-- 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 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-4 h-4"></i>
|
||||
<span>Producten</span>
|
||||
<!-- 2. Assortiment (Grouped Dropdown) -->
|
||||
<div class="relative shrink-0" @click.outside="catalogOpen = false">
|
||||
<button
|
||||
type="button"
|
||||
@click="catalogOpen = !catalogOpen; relationsOpen = false"
|
||||
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' }}"
|
||||
>
|
||||
<i data-lucide="box" class="w-3.5 h-3.5"></i>
|
||||
<span>Assortiment</span>
|
||||
@if($draftBadgeCount > 0)
|
||||
<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 }}
|
||||
</span>
|
||||
@endif
|
||||
<i data-lucide="chevron-down" class="w-3 h-3 text-slate-400 transition-transform duration-200" :class="catalogOpen ? 'rotate-180' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<div
|
||||
x-show="catalogOpen"
|
||||
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>
|
||||
|
||||
@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>
|
||||
<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' }}">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<i data-lucide="layers" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<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="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.5 rounded-full text-[10px] font-semibold bg-amber-100 text-amber-800">
|
||||
{{ $draftBadgeCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
|
||||
<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' }}">
|
||||
<i data-lucide="folder-tree" class="w-4 h-4"></i>
|
||||
<span>Categorieën</span>
|
||||
<div class="my-1 border-t border-slate-100"></div>
|
||||
|
||||
<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' }}">
|
||||
<i data-lucide="folder-tree" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<div>
|
||||
<div class="font-medium text-slate-900">Categorieën</div>
|
||||
<div class="text-[11px] text-slate-500">Winkelindeling beheren</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' }}">
|
||||
<i data-lucide="shopping-cart" class="w-4 h-4"></i>
|
||||
<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 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' }}">
|
||||
<i data-lucide="download-cloud" class="w-4 h-4"></i>
|
||||
<span>MakerWorld Sync</span>
|
||||
</a>
|
||||
</nav>
|
||||
<!-- 4. Klanten & Communicatie (Grouped Dropdown) -->
|
||||
<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-3">
|
||||
<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">
|
||||
<span>Naar Webshop</span>
|
||||
<i data-lucide="external-link" class="w-3.5 h-3.5"></i>
|
||||
<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 -->
|
||||
<button @click="mobileNav = !mobileNav" type="button" class="md:hidden p-2 rounded-xl bg-slate-800 text-slate-200 hover:bg-slate-700">
|
||||
<!-- 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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -90,54 +250,109 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Drawer -->
|
||||
<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">
|
||||
<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' }}">
|
||||
🎨 Layout & Teksten
|
||||
<!-- Mobile Drawer (Clean, Grouped Sections) -->
|
||||
<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">
|
||||
<div>
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-3 block mb-1">Content & Layout</span>
|
||||
<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' }}">
|
||||
Layout & Teksten
|
||||
</a>
|
||||
<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' }}">
|
||||
📦 Producten
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-3 block mb-1">Assortiment</span>
|
||||
<div class="space-y-0.5">
|
||||
<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' }}">
|
||||
Producten
|
||||
</a>
|
||||
<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)
|
||||
<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' }}">
|
||||
<span>Artikel Beheer (Geïmporteerd)</span>
|
||||
@if(($draftBadgeCount ?? 0) > 0)
|
||||
<span class="ml-2 px-1.5 py-0.5 rounded-full text-xs font-black bg-amber-400 text-slate-950">
|
||||
{{ $draftBadgeCount }} nieuw
|
||||
<span class="px-1.5 py-0.5 rounded-full text-[10px] font-semibold bg-slate-200 text-slate-800">
|
||||
{{ $draftBadgeCount }}
|
||||
</span>
|
||||
@endif
|
||||
</a>
|
||||
<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' }}">
|
||||
📁 Categorieën
|
||||
<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' }}">
|
||||
Categorieën
|
||||
</a>
|
||||
<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' }}">
|
||||
🛒 Bestellingen
|
||||
<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>
|
||||
<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' }}">
|
||||
🤖 MakerWorld Importer
|
||||
</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 & Profielen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 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 -->
|
||||
@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">
|
||||
<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>
|
||||
</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">
|
||||
<span>Bekijk Live</span>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
<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">
|
||||
Bekijk Live Shop
|
||||
</a>
|
||||
</div>
|
||||
@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'))
|
||||
<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">
|
||||
<i data-lucide="alert-circle" class="w-5 h-5 text-rose-400"></i>
|
||||
<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-4 h-4 text-rose-600 shrink-0"></i>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@@ -145,10 +360,9 @@
|
||||
@yield('content')
|
||||
</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 • Direct gekoppeld aan live webshop
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -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!'))">
|
||||
<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.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'])
|
||||
|
||||
<style>
|
||||
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 {
|
||||
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>
|
||||
</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) -->
|
||||
@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="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">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span>{{ setting('announcement_text', 'Nederlandse 3D Printstudio • Haarscherpe afwerking op Bambu Lab printers') }}</span>
|
||||
<!-- 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="hidden md:flex items-center space-x-5 text-[11px] text-slate-300">
|
||||
<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')
|
||||
<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="flex items-center space-x-2 mx-auto sm:mx-0">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<span class="text-slate-300 font-medium">{{ setting('announcement_text', 'Nederlandse 3D Printstudio • Haarscherpe afwerking op Bambu Lab printers') }}</span>
|
||||
</div>
|
||||
<div class="hidden md:flex items-center space-x-5 text-slate-400">
|
||||
@if(setting('announcement_badge_1'))
|
||||
<span class="flex items-center space-x-1">
|
||||
<i data-lucide="truck" class="w-3.5 h-3.5 text-amber-400"></i>
|
||||
<span class="flex items-center space-x-1.5">
|
||||
<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>
|
||||
@endif
|
||||
@if(setting('announcement_badge_2'))
|
||||
<span class="flex items-center space-x-1">
|
||||
<i data-lucide="clock" class="w-3.5 h-3.5 text-sky-400"></i>
|
||||
<span class="flex items-center space-x-1.5">
|
||||
<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>
|
||||
@endif
|
||||
@@ -51,62 +74,65 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Main Navigation Header -->
|
||||
<header class="sticky top-0 z-40 bg-white/95 backdrop-blur-md border-b border-slate-200/80 shadow-xs">
|
||||
<!-- Modern Glass Navigation Bar -->
|
||||
<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="flex items-center justify-between h-16 sm:h-20">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
|
||||
<!-- Catchy Brand Logo -->
|
||||
<div class="flex items-center space-x-4 sm:space-x-8">
|
||||
<!-- Brand Logo -->
|
||||
<div class="flex items-center space-x-8">
|
||||
<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>
|
||||
|
||||
<!-- Desktop Nav Links -->
|
||||
<nav class="hidden lg:flex items-center space-x-1.5 text-sm font-bold text-slate-700">
|
||||
<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' }}">
|
||||
<!-- Minimalist Navigation Links -->
|
||||
<nav class="hidden lg:flex items-center space-x-1 text-xs font-medium text-slate-600">
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ request('q') }}"
|
||||
placeholder="Zoek print, 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"
|
||||
placeholder="Zoek model..."
|
||||
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>
|
||||
</form>
|
||||
|
||||
<!-- Cart Button with Counter -->
|
||||
<!-- Cart Button with Pill Badge -->
|
||||
@php
|
||||
$cartItems = is_array(session('cart')) ? session('cart') : [];
|
||||
$cartCount = array_sum(array_column($cartItems, 'quantity'));
|
||||
@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">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4 text-slate-900"></i>
|
||||
<span class="hidden sm:inline">Winkelwagen</span>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span class="hidden sm:inline">Winkelmand</span>
|
||||
@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 }}
|
||||
</span>
|
||||
@endif
|
||||
@@ -114,34 +140,36 @@
|
||||
|
||||
<!-- User Account / Login Button -->
|
||||
@guest
|
||||
<a href="{{ route('login') }}" class="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold text-xs flex items-center space-x-1.5 transition">
|
||||
<i data-lucide="user" class="w-4 h-4 text-slate-700"></i>
|
||||
<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-3.5 h-3.5 text-slate-500"></i>
|
||||
<span class="hidden sm:inline">Inloggen</span>
|
||||
</a>
|
||||
@else
|
||||
<div class="relative" x-data="{ userMenu: false }">
|
||||
<button @click="userMenu = !userMenu" type="button" class="px-3 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-900 font-black text-xs flex items-center space-x-1.5 transition cursor-pointer">
|
||||
<i data-lucide="user-check" class="w-4 h-4 text-amber-500"></i>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div x-show="userMenu" @click.away="userMenu = false" x-cloak class="absolute right-0 mt-2 w-48 rounded-2xl bg-white border border-slate-200 shadow-xl py-2 z-50 text-xs font-bold text-slate-700">
|
||||
<a href="{{ route('account.index') }}" class="flex items-center space-x-2 px-4 py-2 hover:bg-slate-50 text-slate-800">
|
||||
<i data-lucide="user-circle" class="w-4 h-4 text-slate-500"></i>
|
||||
<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-3.5 py-2 hover:bg-slate-50 text-slate-700">
|
||||
<i data-lucide="user" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<span>Mijn Account</span>
|
||||
</a>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<a href="{{ route('admin.content.index') }}" class="flex items-center space-x-2 px-4 py-2 hover:bg-amber-50 text-amber-900 font-black">
|
||||
<i data-lucide="shield" class="w-4 h-4 text-amber-500"></i>
|
||||
<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-3.5 h-3.5 text-sky-600"></i>
|
||||
<span>Admin Beheer</span>
|
||||
</a>
|
||||
@endif
|
||||
<div class="border-t border-slate-100 my-1"></div>
|
||||
<form action="{{ route('logout') }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="w-full text-left flex items-center space-x-2 px-4 py-2 hover:bg-rose-50 text-rose-600 cursor-pointer">
|
||||
<i data-lucide="log-out" class="w-4 h-4"></i>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Uitloggen</span>
|
||||
</button>
|
||||
</form>
|
||||
@@ -150,7 +178,7 @@
|
||||
@endguest
|
||||
|
||||
<!-- Mobile Menu Trigger -->
|
||||
<button @click="mobileMenuOpen = !mobileMenuOpen" type="button" class="lg:hidden p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 transition">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -158,65 +186,61 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ request('q') }}"
|
||||
placeholder="Zoek print, 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"
|
||||
placeholder="Zoek model..."
|
||||
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>
|
||||
</form>
|
||||
<div class="flex flex-col space-y-1.5 font-bold text-sm">
|
||||
<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' : '' }}">
|
||||
<div class="flex flex-col space-y-1 font-medium text-xs">
|
||||
<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>
|
||||
</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 class="text-xs bg-violet-100 px-2 py-0.5 rounded-md font-bold">Bestsellers</span>
|
||||
</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 class="text-xs bg-amber-100 px-2 py-0.5 rounded-md font-bold">Populair</span>
|
||||
</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>
|
||||
</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>
|
||||
</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
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<a href="{{ route('login') }}" class="py-2.5 px-3 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-800 text-center font-bold text-xs flex items-center justify-center space-x-1.5">
|
||||
<i data-lucide="log-in" class="w-3.5 h-3.5"></i>
|
||||
<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">
|
||||
<span>Inloggen</span>
|
||||
</a>
|
||||
<a href="{{ route('register') }}" class="py-2.5 px-3 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 text-center font-black text-xs flex items-center justify-center space-x-1.5">
|
||||
<i data-lucide="user-plus" class="w-3.5 h-3.5"></i>
|
||||
<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">
|
||||
<span>Registreren</span>
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-1">
|
||||
<a href="{{ route('account.index') }}" class="px-3.5 py-2.5 rounded-xl hover:bg-slate-50 flex items-center justify-between text-slate-900 font-bold">
|
||||
<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>
|
||||
<i data-lucide="user" class="w-4 h-4 text-amber-500"></i>
|
||||
</a>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<a href="{{ route('admin.content.index') }}" class="px-3.5 py-2.5 rounded-xl bg-amber-50 hover:bg-amber-100 flex items-center justify-between text-amber-950 font-black">
|
||||
<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>
|
||||
<i data-lucide="shield" class="w-4 h-4 text-amber-600"></i>
|
||||
</a>
|
||||
@endif
|
||||
<form action="{{ route('logout') }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="w-full text-left px-3.5 py-2.5 rounded-xl hover:bg-rose-50 text-rose-600 font-bold flex items-center justify-between">
|
||||
<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>
|
||||
<i data-lucide="log-out" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -229,19 +253,19 @@
|
||||
<!-- Global Flash Alerts -->
|
||||
@if(session('success'))
|
||||
<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">
|
||||
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-600"></i>
|
||||
<span>{{ session('success') }}</span>
|
||||
</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 →</a>
|
||||
<a href="{{ route('cart.index') }}" class="text-xs font-semibold text-emerald-700 hover:underline">Winkelwagen →</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<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>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@@ -253,81 +277,87 @@
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<!-- Footer (Licht, Helder, Aanpasbaar via Admin) -->
|
||||
<footer class="bg-slate-900 text-white border-t border-slate-800 pt-12 pb-10 mt-16">
|
||||
<!-- Fresh, Minimalist, Modern Footer -->
|
||||
<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="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 -->
|
||||
<div class="space-y-3 sm:col-span-1">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-8 w-auto brightness-200 invert">
|
||||
</div>
|
||||
<p class="text-xs text-slate-300 leading-relaxed font-medium">
|
||||
<a href="{{ route('home') }}" class="inline-block">
|
||||
<img src="/images/logo.svg" alt="{{ setting('site_name', 'Jouwdriedee.nl') }}" class="h-7 w-auto">
|
||||
</a>
|
||||
<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.') }}
|
||||
</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">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
|
||||
<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-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
<span>{{ setting('footer_status_text', 'Printers gekalibreerd & actief') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Col 2: Categorieën -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-amber-300">Collectie</h3>
|
||||
<ul class="space-y-1.5 text-xs text-slate-300 font-semibold">
|
||||
<li><a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="hover:text-white 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' => 'film-popculture']) }}" class="hover:text-white transition">Film, Gaming & Collectibles</a></li>
|
||||
<li><a href="{{ route('catalog') }}" class="hover:text-white transition">Alle Prints Bekijken</a></li>
|
||||
<div class="space-y-2.5">
|
||||
<h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Collecties</h3>
|
||||
<ul class="space-y-2 text-xs text-slate-500 font-normal">
|
||||
<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-slate-900 transition">Pokémon 3D Prints</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-slate-900 transition">Alle Prints Bekijken</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Col 3: Materialen & Kwaliteit -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-sky-300">Materialen & Kwaliteit</h3>
|
||||
<ul class="space-y-1.5 text-xs text-slate-300 font-medium">
|
||||
<div class="space-y-2.5">
|
||||
<h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Precisie & Materialen</h3>
|
||||
<ul class="space-y-2 text-xs text-slate-500 font-normal">
|
||||
<li class="flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||
<span><strong>Bio-PLA+</strong>: Biologisch & geurloos</span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
|
||||
<span><strong>Bio-PLA+</strong>: Biologisch & reukloos</span>
|
||||
</li>
|
||||
<li class="flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-sky-400"></span>
|
||||
<span><strong>PETG Tough</strong>: Slagvast & stevig</span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-sky-400"></span>
|
||||
<span><strong>PETG Tough</strong>: Slagvast & hittebestendig</span>
|
||||
</li>
|
||||
<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>
|
||||
</li>
|
||||
<li class="flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-purple-400"></span>
|
||||
<span><strong>Silk Glans</strong>: Prachtige glans</span>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
|
||||
<span><strong>Silk Glans</strong>: Luxe zijdeglans</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Col 4: Contact -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="font-heading text-xs font-black uppercase tracking-wider text-rose-300">Bestelling & Verzending</h3>
|
||||
<p class="text-xs text-slate-300 font-medium">Heb je een vraag over een print of levertijd van jouw bestelling?</p>
|
||||
<div class="p-3 rounded-2xl bg-slate-800/80 border border-slate-700 text-xs space-y-1 font-bold">
|
||||
<div class="flex items-center space-x-1.5 text-slate-200">
|
||||
<i data-lucide="mail" class="w-3.5 h-3.5 text-amber-400"></i>
|
||||
<div class="space-y-2.5">
|
||||
<h3 class="text-xs font-semibold text-slate-900 tracking-wider uppercase text-[11px]">Service & Contact</h3>
|
||||
<p class="text-xs text-slate-500 leading-relaxed font-normal">Vragen over custom printopdrachten of bestellingen?</p>
|
||||
<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-2 text-slate-700">
|
||||
<i data-lucide="mail" class="w-3.5 h-3.5 text-slate-400"></i>
|
||||
<span>{{ setting('footer_email', 'info@jouwdriedee.nl') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1.5 text-slate-200">
|
||||
<i data-lucide="truck" class="w-3.5 h-3.5 text-amber-400"></i>
|
||||
<div class="flex items-center space-x-2 text-slate-700">
|
||||
<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>
|
||||
</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>→</span>
|
||||
</a>
|
||||
</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>© {{ 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">
|
||||
<span>{{ setting('footer_payment_text', 'iDEAL & Bancontact • PostNL Track & Trace') }}</span>
|
||||
<div class="text-slate-400 text-[11px]">
|
||||
{{ setting('footer_payment_text', 'iDEAL & Bancontact • PostNL Track & Trace') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,46 +3,45 @@
|
||||
@section('title', '3D Print Catalogus | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<!-- Page Header -->
|
||||
<div class="bg-slate-50 border-b border-slate-200/80 py-6 sm:py-8">
|
||||
<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">
|
||||
<!-- Page Header (Fris & Minimalistisch) -->
|
||||
<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-4">
|
||||
<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>Overzicht 3D Modellen</span>
|
||||
</div>
|
||||
<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>
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-slate-400">Assortiment</span>
|
||||
<h1 class="font-heading text-2xl sm:text-4xl font-extrabold text-slate-900 tracking-tight mt-0.5">3D Print Collectie</h1>
|
||||
<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>
|
||||
</div>
|
||||
@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">
|
||||
<span>✕ Filters Wissen</span>
|
||||
<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">
|
||||
<i data-lucide="x" class="w-3.5 h-3.5"></i>
|
||||
<span>Filters wissen</span>
|
||||
</a>
|
||||
@endif
|
||||
</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!) -->
|
||||
<div class="lg:hidden mb-4 flex items-center justify-between gap-2">
|
||||
<!-- Mobile Filter Toggle Bar -->
|
||||
<div class="lg:hidden mb-6 flex items-center justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@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>
|
||||
<span x-text="mobileFiltersOpen ? 'Verberg Filters' : 'Filters & Categorieën'"></span>
|
||||
<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>
|
||||
@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
|
||||
</button>
|
||||
|
||||
<!-- Snelle Sortering op Mobiel -->
|
||||
<form action="{{ route('catalog') }}" method="GET" class="w-40">
|
||||
<!-- Quick Sort on Mobile -->
|
||||
<form action="{{ route('catalog') }}" method="GET" class="w-36">
|
||||
@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('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="price_asc" {{ request('sort') === 'price_asc' ? 'selected' : '' }}>Prijs ↑</option>
|
||||
<option value="price_desc" {{ request('sort') === 'price_desc' ? 'selected' : '' }}>Prijs ↓</option>
|
||||
@@ -50,35 +49,36 @@
|
||||
</form>
|
||||
</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">
|
||||
<span class="font-heading text-xs font-black text-slate-900 uppercase tracking-wider flex items-center space-x-1.5">
|
||||
<i data-lucide="filter" class="w-3.5 h-3.5 text-rose-500"></i>
|
||||
<span>Filters & Zoeken</span>
|
||||
<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-slate-400"></i>
|
||||
<span>Filters</span>
|
||||
</span>
|
||||
@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
|
||||
</div>
|
||||
|
||||
<!-- 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('material')) <input type="hidden" name="material" value="{{ request('material') }}"> @endif
|
||||
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ request('q') }}"
|
||||
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>
|
||||
</div>
|
||||
@@ -86,13 +86,13 @@
|
||||
|
||||
<!-- Categorie filter -->
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 mb-2">Categorieën</label>
|
||||
<div class="space-y-1">
|
||||
<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' }}">
|
||||
<label class="block text-xs font-medium text-slate-700 mb-2">Categorieën</label>
|
||||
<div class="space-y-0.5">
|
||||
<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>
|
||||
</a>
|
||||
@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>
|
||||
</a>
|
||||
@endforeach
|
||||
@@ -101,13 +101,13 @@
|
||||
|
||||
<!-- Materiaal filter -->
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 mb-2">Filament Materiaal</label>
|
||||
<div class="space-y-1">
|
||||
<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' }}">
|
||||
<label class="block text-xs font-medium text-slate-700 mb-2">Filament Materiaal</label>
|
||||
<div class="space-y-0.5">
|
||||
<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>
|
||||
</a>
|
||||
@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>
|
||||
</a>
|
||||
@endforeach
|
||||
@@ -116,8 +116,8 @@
|
||||
|
||||
<!-- Sortering Desktop -->
|
||||
<div class="hidden lg:block">
|
||||
<label class="block text-xs font-bold 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">
|
||||
<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-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="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>
|
||||
@@ -127,54 +127,55 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Producten Overzicht (2 kolommen op mobiel, 3 op desktop) -->
|
||||
<!-- Producten Overzicht -->
|
||||
<div class="lg:col-span-3 space-y-6">
|
||||
@if($products->isEmpty())
|
||||
<div class="p-10 text-center rounded-3xl bg-white border border-slate-100 shadow-sm 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="p-12 text-center rounded-2xl bg-white border border-slate-200/60 shadow-2xs space-y-3">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@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)
|
||||
<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 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
|
||||
src="{{ $product->image_url }}"
|
||||
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"
|
||||
>
|
||||
@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 }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3 sm:p-4">
|
||||
<h3 class="font-heading text-xs sm:text-sm font-black text-slate-900 group-hover:text-rose-600 transition line-clamp-2">
|
||||
<div class="pt-3 pb-1">
|
||||
<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) }}">
|
||||
{{ $product->name }}
|
||||
</a>
|
||||
</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 }}
|
||||
</p>
|
||||
</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>
|
||||
<span class="text-[9px] sm:text-[10px] text-slate-400 block font-bold uppercase">Vanaf</span>
|
||||
<span class="font-heading text-sm sm:text-base font-black text-slate-900">{{ $product->formatted_price }}</span>
|
||||
<span class="text-[10px] text-slate-400 block font-normal leading-none">Vanaf</span>
|
||||
<span class="font-heading text-sm font-bold text-slate-900">{{ $product->formatted_price }}</span>
|
||||
</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">
|
||||
<span>Kleur 🎨</span>
|
||||
<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>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Eigen 3D Model Printen (STL / 3MF) | Jouwdriedee.nl')
|
||||
@section('title', 'Eigen 3D Model Printen | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<!-- Header Banner -->
|
||||
<div class="bg-slate-50 border-b border-slate-200/80 py-8 sm:py-12">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-3">
|
||||
<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">
|
||||
<i data-lucide="layers" class="w-3.5 h-3.5 text-rose-500"></i>
|
||||
<span>3D Print op Aanvraag & Maatwerk</span>
|
||||
<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="layers" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>3D Print op Aanvraag</span>
|
||||
</span>
|
||||
<h1 class="font-heading text-3xl sm:text-5xl font-black text-slate-900 tracking-tight">Eigen 3D Model Printen</h1>
|
||||
<p class="text-xs sm:text-sm text-slate-600 max-w-xl mx-auto leading-relaxed font-normal">
|
||||
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.
|
||||
<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-500 max-w-xl mx-auto leading-relaxed">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12"
|
||||
x-data="{
|
||||
activeTab: 'direct', // 'direct' of 'offerte'
|
||||
activeTab: 'direct',
|
||||
selectedSize: 'medium',
|
||||
sizePrices: {
|
||||
'klein': 9.95, // tot 8 cm
|
||||
'medium': 16.95, // tot 15 cm
|
||||
'groot': 24.95 // tot 22 cm
|
||||
'klein': 9.95,
|
||||
'medium': 16.95,
|
||||
'groot': 24.95
|
||||
},
|
||||
selectedMaterial: 'pla',
|
||||
materialMultipliers: {
|
||||
@@ -32,7 +32,7 @@
|
||||
'{{ $mat->code }}': {{ $mat->price_multiplier }},
|
||||
@endforeach
|
||||
},
|
||||
selectedColor: '{{ $colors[0]->name ?? 'Zon Geel (Pikachu)' }}',
|
||||
selectedColor: '{{ $colors[0]->name ?? 'Zwart' }}',
|
||||
quantity: 1,
|
||||
|
||||
get calculatedTotal() {
|
||||
@@ -48,75 +48,75 @@
|
||||
}">
|
||||
|
||||
<!-- 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
|
||||
type="button"
|
||||
@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="flex-1 py-2.5 px-4 rounded-xl text-xs transition flex items-center justify-center space-x-1.5"
|
||||
: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-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>
|
||||
<span>Direct online bestellen</span>
|
||||
<span>Direct bestellen</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@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="flex-1 py-2.5 px-4 rounded-xl text-xs transition flex items-center justify-center space-x-1.5"
|
||||
: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-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>
|
||||
<span>Maatwerk offerte</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- TAB 1: DIRECT BESTELLEN EN IN WINKELWAGEN -->
|
||||
<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">
|
||||
<!-- TAB 1: DIRECT BESTELLEN -->
|
||||
<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">
|
||||
@csrf
|
||||
|
||||
<!-- 1. Model bron: Bestand of Link -->
|
||||
<div class="space-y-4">
|
||||
<div class="border-b border-slate-100 pb-3">
|
||||
<h3 class="font-heading text-base font-black text-slate-900 flex items-center space-x-2">
|
||||
<span>1. 3D-bestand uploaden of link invoeren</span>
|
||||
<div class="space-y-3">
|
||||
<div class="border-b border-slate-100 pb-2">
|
||||
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
|
||||
1. 3D-bestand uploaden of link invoeren
|
||||
</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 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 -->
|
||||
<div class="p-4 rounded-2xl bg-slate-50 border border-slate-200/80 hover:border-slate-300 transition">
|
||||
<label class="block text-xs font-bold 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>
|
||||
<span>Bestand uploaden (.stl, .3mf, .step, .obj)</span>
|
||||
<div class="p-4 rounded-xl bg-slate-50 border border-slate-200 hover:border-slate-300 transition">
|
||||
<label class="block text-xs font-semibold text-slate-800 mb-1 flex items-center space-x-1.5">
|
||||
<i data-lucide="file-up" class="w-3.5 h-3.5 text-slate-500"></i>
|
||||
<span>Bestand uploaden (.stl, .3mf, .step)</span>
|
||||
</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">
|
||||
<span class="text-[10px] text-slate-400 block mt-1">Maximale bestandsgrootte: 50MB</span>
|
||||
<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">Tot 50 MB per bestand</span>
|
||||
</div>
|
||||
|
||||
<!-- Of URL -->
|
||||
<div class="p-4 rounded-2xl bg-slate-50 border border-slate-200/80 hover:border-slate-300 transition">
|
||||
<label class="block text-xs font-bold 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>
|
||||
<div class="p-4 rounded-xl bg-slate-50 border border-slate-200 hover:border-slate-300 transition">
|
||||
<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-slate-500"></i>
|
||||
<span>Of plak een MakerWorld / Printables link</span>
|
||||
</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">
|
||||
<span class="text-[10px] text-slate-400 block mt-1">Wij downloaden en inspecteren het bestand</span>
|
||||
<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 halen de specificaties automatisch op</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Omschrijving of modelnaam (optioneel)</label>
|
||||
<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>
|
||||
|
||||
<!-- 2. Formaat & Afmeting -->
|
||||
<div class="space-y-3">
|
||||
<div class="border-b border-slate-100 pb-2">
|
||||
<h3 class="font-heading text-base font-black text-slate-900">
|
||||
<span>2. Formaat en schaal selecteren</span>
|
||||
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
|
||||
2. Formaat en schaal selecteren
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -125,57 +125,57 @@
|
||||
<button
|
||||
type="button"
|
||||
@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="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
: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-xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
<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">€ 9,95</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@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="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
: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-xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
<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">€ 16,95</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@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="p-4 rounded-2xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
: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-xl border text-left transition cursor-pointer flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<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>
|
||||
<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">€ 24,95</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Materiaal & Kleur -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="border-b border-slate-100 pb-2">
|
||||
<h3 class="font-heading text-base font-black text-slate-900">
|
||||
<span>3. Materiaal en printkleur</span>
|
||||
<h3 class="text-xs font-bold text-slate-900 uppercase tracking-wider">
|
||||
3. Materiaal en printkleur
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<!-- Materiaal -->
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 mb-1.5">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">
|
||||
<div class="space-y-1">
|
||||
<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 text-xs font-medium text-slate-900 focus:bg-white focus:outline-none">
|
||||
@foreach($materials as $mat)
|
||||
<option value="{{ $mat->code }}">{{ $mat->name }}</option>
|
||||
@endforeach
|
||||
@@ -183,57 +183,54 @@
|
||||
</div>
|
||||
|
||||
<!-- Kleur -->
|
||||
<div>
|
||||
<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>
|
||||
<div class="space-y-1">
|
||||
<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">
|
||||
<div class="flex flex-wrap gap-2 pt-1">
|
||||
<div class="flex flex-wrap gap-1.5 pt-1">
|
||||
@foreach($colors as $c)
|
||||
<button
|
||||
type="button"
|
||||
@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="w-7 h-7 rounded-xl border shadow-2xs transition flex items-center justify-center cursor-pointer relative"
|
||||
:class="selectedColor === '{{ $c->name }}' ? 'ring-2 ring-slate-900 ring-offset-1' : 'hover:scale-105 border-slate-200'"
|
||||
class="w-6 h-6 rounded-full border shadow-2xs transition flex items-center justify-center cursor-pointer"
|
||||
style="background-color: {{ $c->hex }};"
|
||||
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>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 mb-1">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>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Bijzondere instructies (optioneel)</label>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider block">Berekende Stukprijs</span>
|
||||
<div class="flex items-baseline space-x-2">
|
||||
<span class="font-heading text-3xl font-black text-slate-900">€ <span x-text="unitPrice"></span></span>
|
||||
<span class="text-xs text-slate-500">incl. btw & materiaal</span>
|
||||
<span class="text-[10px] font-semibold text-slate-400 uppercase tracking-wider block">Berekende stukprijs</span>
|
||||
<div class="flex items-baseline space-x-1.5">
|
||||
<span class="text-2xl font-bold text-slate-900">€ <span x-text="unitPrice"></span></span>
|
||||
<span class="text-xs text-slate-500">incl. btw</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3 w-full sm:w-auto">
|
||||
<!-- Aantal -->
|
||||
<div class="flex items-center border border-slate-200 rounded-xl bg-white overflow-hidden shadow-2xs">
|
||||
<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>
|
||||
<span class="px-3 py-2 font-heading font-black text-xs text-slate-900" x-text="quantity"></span>
|
||||
<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-2.5 py-1 text-slate-600 font-bold cursor-pointer hover:bg-slate-50">-</button>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4"></i>
|
||||
<span>In Winkelwagen • € <span x-text="calculatedTotal"></span></span>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>In winkelwagen • € <span x-text="calculatedTotal"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,48 +239,48 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TAB 2: VRIJBLIJVENDE OFFERTE (VOOR MAATWERK & ADVIES) -->
|
||||
<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">
|
||||
<!-- TAB 2: VRIJBLIJVENDE OFFERTE -->
|
||||
<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">
|
||||
<h3 class="font-heading text-lg font-black 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>
|
||||
<h3 class="text-base font-bold text-slate-900">Vrijblijvende Maatwerk Offerte</h3>
|
||||
<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>
|
||||
|
||||
<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
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">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">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Naam *</label>
|
||||
<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>
|
||||
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<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 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">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">
|
||||
<div class="space-y-1">
|
||||
<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 py-2 text-xs text-slate-900 focus:bg-white focus:outline-none">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">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">
|
||||
<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">
|
||||
<p class="text-[11px] text-slate-400 mt-2">Ondersteunde formaten: STL, STEP, OBJ, 3MF of ZIP (max 50 MB)</p>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">3D Model Bestand (.stl, .step, .obj, .zip)</label>
|
||||
<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-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-[10px] text-slate-400 mt-1.5">STL, STEP, OBJ, 3MF of ZIP (max 50 MB)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-slate-700 uppercase tracking-wider mb-1">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>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-xs font-medium text-slate-700">Toelichting, Afmetingen & Aantal *</label>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<i data-lucide="send" class="w-4 h-4"></i>
|
||||
<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-3.5 h-3.5"></i>
|
||||
<span>Aanvraag Verzenden</span>
|
||||
</button>
|
||||
</form>
|
||||
@@ -291,4 +288,3 @@
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -3,62 +3,62 @@
|
||||
@section('title', setting('site_name', 'Jouwdriedee.nl') . ' | ' . setting('site_tagline', 'Kleurrijke 3D Prints, Fidget Toys & Collectibles'))
|
||||
|
||||
@section('content')
|
||||
<!-- Hero Section (Helder, Kleurrijk, Aanpasbaar via Admin) -->
|
||||
<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">
|
||||
<div class="relative 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">
|
||||
<!-- Hero Section (Fris, Subtiel, Modern & Ruimtelijk) -->
|
||||
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-center">
|
||||
|
||||
<!-- 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">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
<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-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
<span>{{ setting('hero_badge', 'Nederlandse 3D Printstudio • Bambu Lab Precisie') }}</span>
|
||||
</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.'))) !!}
|
||||
</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!') }}
|
||||
</p>
|
||||
|
||||
<!-- Action CTA's -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center lg:justify-start gap-3 pt-2">
|
||||
<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">
|
||||
<!-- Clean Minimal CTAs -->
|
||||
<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-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>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Technische specificaties / Kwaliteitsbadges -->
|
||||
<div class="pt-4 grid grid-cols-3 gap-2 sm:gap-4 max-w-md mx-auto lg:mx-0">
|
||||
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center">
|
||||
<div class="text-base sm:text-xl font-black text-slate-900 font-heading">
|
||||
<!-- Sleek Micro Spec Badges -->
|
||||
<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>
|
||||
<div class="text-base sm:text-lg font-bold text-slate-900">
|
||||
{{ setting('hero_stat_1_val', '0.12 mm') }}
|
||||
</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') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center">
|
||||
<div class="text-base sm:text-xl font-black text-slate-900 font-heading">
|
||||
<div>
|
||||
<div class="text-base sm:text-lg font-bold text-slate-900">
|
||||
{{ setting('hero_stat_2_val', 'Bambu Lab') }}
|
||||
</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') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-white border border-slate-200/80 rounded-2xl shadow-2xs text-center">
|
||||
<div class="text-base sm:text-xl font-black text-slate-900 font-heading">
|
||||
<div>
|
||||
<div class="text-base sm:text-lg font-bold text-slate-900">
|
||||
{{ setting('hero_stat_3_val', '2-3 Dagen') }}
|
||||
</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') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,41 +66,29 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right Hero Card / Werkelijke 3D Print Productfoto -->
|
||||
<!-- Right Hero Card: Frameless Editorial Showcase -->
|
||||
<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="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">
|
||||
{{ setting('hero_card_badge', 'Meest gekozen print') }}
|
||||
</div>
|
||||
|
||||
<div class="relative h-64 sm:h-84 rounded-2xl overflow-hidden bg-slate-100">
|
||||
<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 hover:scale-105 transition-transform duration-500"
|
||||
class="w-full h-full object-cover object-center group-hover:scale-102 transition-transform duration-700 ease-out"
|
||||
>
|
||||
|
||||
<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">
|
||||
<!-- Minimalist Floating Pill -->
|
||||
<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>
|
||||
<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>
|
||||
<span class="text-[10px] font-semibold uppercase tracking-wider text-slate-400 block">Uitgelicht</span>
|
||||
<h3 class="font-heading font-bold 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>
|
||||
<span class="text-[10px] text-slate-400 block">Vanaf</span>
|
||||
<span class="font-heading font-bold text-slate-900 text-xs sm:text-sm">€ 17,95</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between text-[11px] text-slate-500 font-semibold px-1">
|
||||
<span class="flex items-center space-x-1">
|
||||
<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 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">
|
||||
{{ setting('hero_card_badge', 'Meest gekozen print') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,157 +97,145 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3 Categorieën Grid (Plat, Helder & Aanpasbaar) -->
|
||||
<section class="py-12 sm:py-16 bg-white border-b border-slate-200/70">
|
||||
<!-- 3 Categorieën Strip (Schoon, Modern & Subtiel) -->
|
||||
<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="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>
|
||||
<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') }}
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</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">
|
||||
<span>Alle prints bekijken</span>
|
||||
<i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
|
||||
<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>Alles bekijken</span>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
</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 -->
|
||||
<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">
|
||||
<div class="h-48 overflow-hidden bg-violet-50">
|
||||
<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">
|
||||
<a href="{{ route('catalog', ['category' => 'fidget-toys']) }}" class="group block">
|
||||
<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-103 transition-transform duration-500">
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<span class="text-[10px] font-bold text-violet-600 uppercase tracking-wider block">Beweegbare Scharnieren</span>
|
||||
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-violet-700 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>
|
||||
<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 class="pt-3.5 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Fidget Toys & Sensory</h3>
|
||||
<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>
|
||||
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Kristaldraken, wriemel-slakken en compacte cubes met soepele scharnieren.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 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">
|
||||
<div class="h-48 overflow-hidden bg-amber-50">
|
||||
<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">
|
||||
<a href="{{ route('catalog', ['category' => 'pokemon-3d']) }}" class="group block">
|
||||
<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-103 transition-transform duration-500">
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<span class="text-[10px] font-bold text-amber-600 uppercase tracking-wider block">Gedetailleerde Figuren</span>
|
||||
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-amber-700 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>
|
||||
<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 class="pt-3.5 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Pokémon 3D Prints</h3>
|
||||
<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>
|
||||
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Pikachu planters, beweegbare Charizard, Pokéballs en Gengar figuren.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- 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">
|
||||
<div class="h-48 overflow-hidden bg-rose-50">
|
||||
<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">
|
||||
<a href="{{ route('catalog', ['category' => 'film-popculture']) }}" class="group block">
|
||||
<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-103 transition-transform duration-500">
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<span class="text-[10px] font-bold text-rose-500 uppercase tracking-wider block">Collectibles & Display</span>
|
||||
<h3 class="font-heading text-lg font-black text-slate-900 group-hover:text-rose-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>
|
||||
<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 class="pt-3.5 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-heading font-semibold text-base text-slate-900 group-hover:text-sky-600 transition">Film, Gaming & Props</h3>
|
||||
<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>
|
||||
<p class="text-xs text-slate-500 line-clamp-1 font-normal">Lightsaber displays, Mjolnir desk organizers en iconische filmfiguren.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Uitgelichte Prints Showcase -->
|
||||
<section class="py-12 sm:py-16 bg-[#FBFBFA]">
|
||||
<!-- Uitgelichte Prints Showcase (Crisp & Modern Grid) -->
|
||||
<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="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>
|
||||
<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') }}
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</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>
|
||||
<i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 2 Kolommen mobiel, 3 op desktop -->
|
||||
<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 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
@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 class="relative h-44 sm:h-60 overflow-hidden bg-slate-100">
|
||||
<div>
|
||||
<!-- Product Photo Frame -->
|
||||
<div class="relative aspect-square rounded-xl overflow-hidden bg-slate-50">
|
||||
<img
|
||||
src="{{ $product->image_url }}"
|
||||
alt="{{ $product->name }}"
|
||||
class="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-500"
|
||||
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">
|
||||
<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">
|
||||
<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">
|
||||
{{ $product->category->name }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($product->print_time_hours)
|
||||
<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">
|
||||
<i data-lucide="clock" class="w-3 h-3 text-sky-400"></i>
|
||||
<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">
|
||||
<!-- Product Info -->
|
||||
<div class="pt-3 pb-1">
|
||||
<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) }}">
|
||||
{{ $product->name }}
|
||||
</a>
|
||||
</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 }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer met prijs & actieknop -->
|
||||
<div class="pt-2 sm:pt-3 border-t border-slate-100 flex items-center justify-between">
|
||||
<!-- Footer: Price & Direct Link -->
|
||||
<div class="pt-2 border-t border-slate-100 flex items-center justify-between mt-2">
|
||||
<div>
|
||||
<span class="text-[9px] sm:text-[10px] text-slate-400 font-bold uppercase block">Vanaf</span>
|
||||
<span class="font-heading text-sm sm:text-lg font-black text-slate-900">
|
||||
<span class="text-[10px] text-slate-400 block font-normal leading-none">Vanaf</span>
|
||||
<span class="font-heading text-sm font-bold text-slate-900">
|
||||
{{ $product->formatted_price }}
|
||||
</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>
|
||||
<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>
|
||||
@endforeach
|
||||
@@ -267,80 +243,80 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Heldere Kwaliteits & Voordelen Banner (Aanpasbaar via Admin) -->
|
||||
<section class="py-12 sm:py-16 bg-white border-t border-slate-200/80">
|
||||
<!-- Subtiele USP's (Puur, Wit & Ruimtelijk) -->
|
||||
<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="text-center max-w-2xl mx-auto mb-10">
|
||||
<span class="text-xs font-bold uppercase tracking-wider text-rose-500">
|
||||
<div class="text-center max-w-xl mx-auto mb-14">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||||
{{ setting('usp_tag', 'Waarom Jouwdriedee.nl?') }}
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
<!-- Feature 1 -->
|
||||
<div class="p-5 rounded-3xl bg-[#FAF7FD] border border-violet-100 text-left 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">
|
||||
<i data-lucide="printer" class="w-5 h-5"></i>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<!-- USP 1 -->
|
||||
<div class="space-y-2">
|
||||
<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-4 h-4"></i>
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 2 -->
|
||||
<div class="p-5 rounded-3xl bg-[#FFFBF0] border border-amber-100 text-left 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">
|
||||
<i data-lucide="palette" class="w-5 h-5"></i>
|
||||
<!-- USP 2 -->
|
||||
<div class="space-y-2">
|
||||
<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-4 h-4"></i>
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 3 -->
|
||||
<div class="p-5 rounded-3xl bg-[#F0FDF4] border border-emerald-100 text-left 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">
|
||||
<i data-lucide="shield-check" class="w-5 h-5"></i>
|
||||
<!-- USP 3 -->
|
||||
<div class="space-y-2">
|
||||
<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-4 h-4"></i>
|
||||
</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+') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feature 4 -->
|
||||
<div class="p-5 rounded-3xl bg-[#EFF6FF] border border-sky-100 text-left 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">
|
||||
<i data-lucide="truck" class="w-5 h-5"></i>
|
||||
<!-- USP 4 -->
|
||||
<div class="space-y-2">
|
||||
<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-4 h-4"></i>
|
||||
</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') }}
|
||||
</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.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 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">
|
||||
<div class="mt-12 text-center">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
+179
-104
@@ -1,24 +1,24 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', $product->name . ' 🎨 | Jouwdriedee.nl')
|
||||
@section('title', $product->name . ' | Jouwdriedee.nl')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="bg-amber-50/50 border-b border-slate-200/80 py-2.5 sm: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">
|
||||
<a href="{{ route('home') }}" class="hover:text-rose-600">Home 🏠</a>
|
||||
<span>/</span>
|
||||
<a href="{{ route('catalog') }}" class="hover:text-rose-600">Catalogus 🎁</a>
|
||||
<!-- Breadcrumbs (Subtiel) -->
|
||||
<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-normal text-slate-500 flex items-center space-x-2 overflow-x-auto whitespace-nowrap">
|
||||
<a href="{{ route('home') }}" class="hover:text-slate-900 transition">Home</a>
|
||||
<span class="text-slate-300">/</span>
|
||||
<a href="{{ route('catalog') }}" class="hover:text-slate-900 transition">Catalogus</a>
|
||||
@if($product->category)
|
||||
<span>/</span>
|
||||
<a href="{{ route('catalog', ['category' => $product->category->slug]) }}" class="hover:text-rose-600">{{ $product->category->name }}</a>
|
||||
<span class="text-slate-300">/</span>
|
||||
<a href="{{ route('catalog', ['category' => $product->category->slug]) }}" class="hover:text-slate-900 transition">{{ $product->category->name }}</a>
|
||||
@endif
|
||||
<span>/</span>
|
||||
<span class="font-black text-slate-900 truncate">{{ $product->name }}</span>
|
||||
<span class="text-slate-300">/</span>
|
||||
<span class="text-slate-900 font-medium truncate">{{ $product->name }}</span>
|
||||
</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="{
|
||||
basePrice: {{ $product->base_price }},
|
||||
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 -->
|
||||
<div class="lg:col-span-6 space-y-4 sm:space-y-6">
|
||||
<div class="rounded-3xl bg-white border border-slate-100 overflow-hidden shadow-sm">
|
||||
<div class="relative h-72 sm:h-96 md:h-112 bg-slate-100 flex items-center justify-center overflow-hidden">
|
||||
<!-- Linkerkant: Productgalerij & Details -->
|
||||
<div class="lg:col-span-7 space-y-6">
|
||||
@php
|
||||
$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
|
||||
src="{{ $product->image_url }}"
|
||||
:src="activeImage"
|
||||
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)
|
||||
<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">
|
||||
<span>🔥</span>
|
||||
<span>Populair</span>
|
||||
<div class="absolute top-3 left-3">
|
||||
<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">
|
||||
Uitgelicht
|
||||
</span>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Print Specificaties Card (Minder lomp, verfijnd) -->
|
||||
<div class="p-4 sm:p-6 rounded-3xl bg-white border border-slate-100 shadow-xs space-y-3">
|
||||
<h3 class="font-heading text-xs font-black text-slate-900 uppercase tracking-wider flex items-center space-x-1.5">
|
||||
<span>🖨️</span>
|
||||
<span>Print Details & Formaat</span>
|
||||
<!-- Print Specificaties Strip (Subtiel) -->
|
||||
<div class="p-5 rounded-2xl bg-slate-50/70 border border-slate-200/60 space-y-4">
|
||||
<h3 class="text-xs font-semibold text-slate-900 uppercase tracking-wider">
|
||||
Print Specificaties
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 text-xs">
|
||||
<div class="p-3 rounded-2xl bg-slate-50 text-center">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-bold">📏 Formaat</span>
|
||||
<span class="font-black text-slate-800 mt-0.5 block font-heading">{{ $product->dimensions ?? 'Standaard' }}</span>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs">
|
||||
<div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-medium">Formaat</span>
|
||||
<span class="font-semibold text-slate-800 mt-0.5 block">{{ $product->dimensions ?? 'Standaard' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-2xl bg-slate-50 text-center">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-bold">⚖️ Gewicht</span>
|
||||
<span class="font-black text-slate-800 mt-0.5 block font-heading">~{{ $product->weight_grams ?? '80' }}g</span>
|
||||
<div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-medium">Gewicht</span>
|
||||
<span class="font-semibold text-slate-800 mt-0.5 block">~{{ $product->weight_grams ?? '80' }}g</span>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-2xl bg-slate-50 text-center">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-bold">⏱️ Printtijd</span>
|
||||
<span class="font-black text-slate-800 mt-0.5 block font-heading">{{ $product->print_time_hours ?? '4.0' }} uur</span>
|
||||
<div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-medium">Printduur</span>
|
||||
<span class="font-semibold text-slate-800 mt-0.5 block">{{ $product->print_time_hours ?? '4.0' }} uur</span>
|
||||
</div>
|
||||
|
||||
<div class="p-3 rounded-2xl bg-slate-50 text-center">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-bold">🔍 Resolutie</span>
|
||||
<span class="font-black text-slate-800 mt-0.5 block font-heading">0.12 - 0.16 mm</span>
|
||||
<div class="p-2.5 rounded-lg bg-white border border-slate-200/50">
|
||||
<span class="text-slate-400 block text-[10px] uppercase font-medium">Resolutie</span>
|
||||
<span class="font-semibold text-slate-800 mt-0.5 block">0.12 - 0.16 mm</span>
|
||||
</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">
|
||||
<span class="flex items-center space-x-1">
|
||||
<span>✨</span>
|
||||
<span>Geprint op precisie Bambu Lab printers</span>
|
||||
</span>
|
||||
<span class="flex items-center space-x-1">
|
||||
<span>🌱</span>
|
||||
<span>Veilig & biologisch afbreekbaar PLA</span>
|
||||
<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.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
|
||||
<span>Geprint op precisie Bambu Lab X1/P1 printers</span>
|
||||
</span>
|
||||
<span>100% biologisch afbreekbaar Bio-PLA+</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Productomschrijving -->
|
||||
<div class="p-4 sm:p-6 rounded-3xl bg-white border border-slate-100 shadow-xs space-y-2">
|
||||
<h3 class="font-heading text-sm font-black text-slate-900 flex items-center space-x-1.5">
|
||||
<span>📝</span>
|
||||
<span>Over deze 3D print</span>
|
||||
<div class="p-5 rounded-2xl bg-white border border-slate-200/60 space-y-3">
|
||||
<h3 class="text-sm font-semibold text-slate-900">
|
||||
Omschrijving
|
||||
</h3>
|
||||
<div class="text-xs text-slate-600 leading-relaxed space-y-1.5 font-medium">
|
||||
{!! nl2br($product->description) !!}
|
||||
<div class="text-xs text-slate-600 leading-relaxed space-y-2 font-normal">
|
||||
{!! nl2br(strip_tags($product->description, '<p><br><strong><b><em><i><u><ul><ol><li><h3><h4>')) !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechterkant: Interactive Configurator & Bestelmodule -->
|
||||
<div class="lg:col-span-6 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">
|
||||
<!-- Rechterkant: Clean Configurator & Bestelmodule -->
|
||||
<div class="lg:col-span-5 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)
|
||||
<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 }}
|
||||
</span>
|
||||
@endif
|
||||
<h1 class="font-heading text-xl sm:text-3xl font-black text-slate-900 tracking-tight">{{ $product->name }}</h1>
|
||||
<p class="text-xs text-slate-500 mt-1 leading-relaxed">{{ $product->short_description }}</p>
|
||||
<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 font-normal leading-relaxed">{{ $product->short_description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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">
|
||||
<span class="font-heading text-2xl sm:text-3xl font-black text-slate-900">€ <span x-text="unitPriceFormatted"></span></span>
|
||||
<span class="text-xs text-slate-500 font-normal">incl. btw</span>
|
||||
<span class="font-heading text-2xl font-extrabold text-slate-900">€ <span x-text="unitPriceFormatted"></span></span>
|
||||
<span class="text-[11px] text-slate-500">incl. btw</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>Print-klaar</span>
|
||||
<span>Op bestelling geprint</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,9 +244,9 @@
|
||||
|
||||
<!-- 1. Materiaal Keuze -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">1. Kies Materiaal</label>
|
||||
<span class="text-xs font-black text-rose-500" x-text="selectedMaterial.toUpperCase()"></span>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-xs font-semibold text-slate-900">1. Materiaal</label>
|
||||
<span class="text-xs font-medium text-slate-500" x-text="selectedMaterial.toUpperCase()"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@foreach($product->available_materials ?? ['pla'] as $matCode)
|
||||
@@ -180,40 +255,40 @@
|
||||
<button
|
||||
type="button"
|
||||
@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"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-bold">{{ $mat->name }}</span>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<span class="text-xs font-medium">{{ $mat->name }}</span>
|
||||
@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
|
||||
</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>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Kleur Selectie (Swatches met mooie afwerking) -->
|
||||
<!-- 2. Kleur Selectie -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">2. Kies Jouw Kleur</label>
|
||||
<span class="text-xs font-bold text-rose-600" x-text="selectedColor"></span>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-xs font-semibold text-slate-900">2. Kleur</label>
|
||||
<span class="text-xs font-medium text-slate-600" x-text="selectedColor"></span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($allColors as $c)
|
||||
<button
|
||||
type="button"
|
||||
@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="w-8 h-8 rounded-xl border shadow-xs transition flex items-center justify-center cursor-pointer relative"
|
||||
: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-7 h-7 rounded-full border shadow-2xs transition flex items-center justify-center cursor-pointer relative"
|
||||
style="background-color: {{ $c->hex }};"
|
||||
title="{{ $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>
|
||||
</button>
|
||||
@endforeach
|
||||
@@ -222,59 +297,59 @@
|
||||
|
||||
<!-- 3. Formaat & Schaal -->
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label class="text-xs font-black text-slate-800 uppercase tracking-wider">3. Formaat / Schaal</label>
|
||||
<span class="text-xs font-bold text-slate-700" x-text="selectedScale"></span>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-xs font-semibold text-slate-900">3. Formaat</label>
|
||||
<span class="text-xs font-medium text-slate-600" x-text="selectedScale"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
@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="p-2.5 rounded-xl border text-center transition cursor-pointer"
|
||||
: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 rounded-lg border text-center transition cursor-pointer"
|
||||
>
|
||||
<span>100% (Normaal)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@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="p-2.5 rounded-xl border text-center transition cursor-pointer"
|
||||
: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 rounded-lg border text-center transition cursor-pointer"
|
||||
>
|
||||
<span>150% (+45%)</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@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="p-2.5 rounded-xl border text-center transition cursor-pointer"
|
||||
: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 rounded-lg border text-center transition cursor-pointer"
|
||||
>
|
||||
<span>200% (+95%)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aantal en Toevoegen Knop -->
|
||||
<div class="pt-3 border-t border-slate-100 space-y-3">
|
||||
<!-- Aantal en Bestelknop -->
|
||||
<div class="pt-2 border-t border-slate-100 space-y-3">
|
||||
<div class="flex items-center space-x-2.5">
|
||||
<!-- Stepper -->
|
||||
<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>
|
||||
<span class="px-3.5 py-2.5 font-heading font-black text-xs 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 > 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 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 hover:bg-slate-200 transition text-slate-700 font-semibold cursor-pointer text-xs">+</button>
|
||||
</div>
|
||||
|
||||
<!-- Knop Desktop -->
|
||||
<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">
|
||||
<span>🛒</span>
|
||||
<span>In Winkelmandje • € <span x-text="calculatedPrice"></span></span>
|
||||
<!-- Bestelknop -->
|
||||
<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">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4"></i>
|
||||
<span>In winkelwagen • € <span x-text="calculatedPrice"></span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center space-x-3 text-[11px] text-slate-500 font-bold pt-1">
|
||||
<span>🚚 Gratis verzending vanaf €50</span>
|
||||
<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>•</span>
|
||||
<span>📦 PostNL Track & Trace</span>
|
||||
<span>PostNL Track & Trace</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ Artisan::command('inspire', function () {
|
||||
$this->comment(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')
|
||||
->everyTenMinutes()
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
+31
-6
@@ -2,7 +2,9 @@
|
||||
|
||||
use App\Http\Controllers\AccountController;
|
||||
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\CustomerController as AdminCustomerController;
|
||||
use App\Http\Controllers\Admin\MakerWorldController;
|
||||
use App\Http\Controllers\Admin\OrderController as AdminOrderController;
|
||||
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\CartController;
|
||||
use App\Http\Controllers\CheckoutController;
|
||||
use App\Http\Controllers\ContactController;
|
||||
use App\Http\Controllers\ShopController;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
@@ -20,8 +23,12 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
// Health & Diagnostics
|
||||
// Health & Diagnostics (Alleen toegankelijk in debug-modus of voor ingelogde admins)
|
||||
Route::get('/health-debug', function () {
|
||||
if (! config('app.debug') && ! (auth()->check() && auth()->user()->isAdmin())) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$dbPath = config('database.connections.sqlite.database');
|
||||
|
||||
$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::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
|
||||
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');
|
||||
|
||||
// Authenticatie & Klantenportaal
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/inloggen', [LoginController::class, 'showLoginForm'])->name('login');
|
||||
Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit');
|
||||
Route::post('/inloggen', [LoginController::class, 'login'])->name('login.submit')->middleware('throttle:5,1');
|
||||
Route::redirect('/login', '/inloggen');
|
||||
|
||||
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::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::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 () {
|
||||
@@ -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/reset', [ContentController::class, 'reset'])->name('content.reset');
|
||||
Route::post('/content/test-mail', [ContentController::class, 'sendTestMail'])->name('content.test-mail');
|
||||
Route::post('/toggle-offline', [ContentController::class, 'toggleOffline'])->name('toggle-offline');
|
||||
|
||||
// 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']);
|
||||
|
||||
// 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::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
|
||||
Route::get('/makerworld', [MakerWorldController::class, 'index'])->name('makerworld.index');
|
||||
Route::post('/makerworld/login', [MakerWorldController::class, 'login'])->name('makerworld.login');
|
||||
|
||||
@@ -7,7 +7,11 @@ use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\PriceCalculatorService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminContentTest extends TestCase
|
||||
@@ -174,4 +178,257 @@ class AdminContentTest extends TestCase
|
||||
$showResponse->assertStatus(200);
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 & 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
@@ -2,9 +2,14 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Setting::flushCache();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user