feat: add webshop offline maintenance mode with admin toggle and red maintenance screen
This commit is contained in:
@@ -85,6 +85,10 @@ class ContentController extends Controller
|
||||
'footer_shipping_text',
|
||||
'footer_copyright',
|
||||
'footer_payment_text',
|
||||
|
||||
// Offline / Maintenance Mode
|
||||
'shop_offline_title',
|
||||
'shop_offline_message',
|
||||
];
|
||||
|
||||
// Handle checkbox toggles that aren't present if unchecked
|
||||
@@ -92,6 +96,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);
|
||||
@@ -168,4 +176,20 @@ class ContentController extends Controller
|
||||
->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()->back()
|
||||
->with('success', "De webshop is direct {$statusText}!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckWebshopOffline;
|
||||
use App\Http\Middleware\EnsureUserIsAdmin;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
@@ -14,6 +15,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->web(append: [
|
||||
CheckWebshopOffline::class,
|
||||
]);
|
||||
$middleware->alias([
|
||||
'admin' => EnsureUserIsAdmin::class,
|
||||
]);
|
||||
|
||||
@@ -107,6 +107,16 @@
|
||||
<i data-lucide="mail" class="w-3.5 h-3.5"></i>
|
||||
<span>7. E-mail & SMTP Testen</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="activeTab = 'offline'"
|
||||
:class="activeTab === 'offline' ? 'bg-rose-600 text-white shadow-sm font-black' : 'bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||
class="px-4 py-2.5 rounded-xl transition flex items-center space-x-1.5 whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full {{ setting('shop_offline', '0') === '1' ? 'bg-rose-400 animate-ping' : 'bg-emerald-400' }}"></span>
|
||||
<span>8. Offline Modus</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ==================================================== -->
|
||||
@@ -631,6 +641,95 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================================================== -->
|
||||
<!-- TAB 8: WEBSHOP OFFLINE MODUS (ONDERHOUDSMODUS) -->
|
||||
<!-- ==================================================== -->
|
||||
<div x-show="activeTab === 'offline'" class="space-y-6">
|
||||
<div class="p-6 rounded-3xl bg-slate-900 border border-slate-800 space-y-6">
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between pb-4 border-b border-slate-800 gap-3">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full {{ setting('shop_offline', '0') === '1' ? 'bg-rose-500 animate-ping' : 'bg-emerald-400' }}"></span>
|
||||
<h3 class="font-heading font-black text-lg text-white">Webshop Offline Modus (Onderhoud)</h3>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-0.5">
|
||||
Wanneer ingeschakeld, kunnen gewone bezoekers niets bestellen en zien zij direct de grote rode offline banner. Alleen ingelogde beheerders kunnen de website bekijken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Directe 1-klik Toggle Knop -->
|
||||
<button
|
||||
type="submit"
|
||||
formaction="{{ route('admin.toggle-offline') }}"
|
||||
formmethod="POST"
|
||||
class="px-5 py-2.5 rounded-xl {{ setting('shop_offline', '0') === '1' ? 'bg-emerald-500 hover:bg-emerald-400 text-slate-950' : 'bg-rose-600 hover:bg-rose-500 text-white' }} font-black text-xs uppercase tracking-wider transition flex items-center justify-center space-x-2 shrink-0 cursor-pointer shadow-lg"
|
||||
>
|
||||
<i data-lucide="power" class="w-4 h-4"></i>
|
||||
<span>{{ setting('shop_offline', '0') === '1' ? 'Zet Webshop Weer ONLINE' : 'Zet Webshop Nu DIRECT OFFLINE' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="shop_offline_submitted" value="1">
|
||||
|
||||
<!-- Toggle Switch -->
|
||||
<div class="p-4 rounded-2xl {{ setting('shop_offline', '0') === '1' ? 'bg-rose-950/40 border-2 border-rose-500/50' : 'bg-slate-950 border border-slate-800' }} flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-xs font-black text-white block">Status Webshop Bereikbaarheid</span>
|
||||
<span class="text-[11px] text-slate-400">
|
||||
{{ setting('shop_offline', '0') === '1' ? 'De webshop is momenteel OFFLINE voor gewone bezoekers.' : 'De webshop is momenteel LIVE en open voor bestellingen.' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="shop_offline" value="1" {{ setting('shop_offline', '0') === '1' ? 'checked' : '' }} class="sr-only peer">
|
||||
<div class="w-14 h-7 bg-slate-800 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[4px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-rose-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Teksten van de Rode Offline Banner -->
|
||||
<div class="grid grid-cols-1 gap-4 pt-2">
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Titel op de Rode Offline Banner</label>
|
||||
<input
|
||||
type="text"
|
||||
name="shop_offline_title"
|
||||
value="{{ setting('shop_offline_title', 'Aan deze webshop wordt hard gewerkt! 🛠️') }}"
|
||||
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-rose-400"
|
||||
>
|
||||
<p class="text-[10px] text-slate-500">De grote rode hoofdtitel die bezoekers in beeld krijgen.</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="block text-xs font-bold text-slate-300">Toelichting / Onderhoudsbericht</label>
|
||||
<textarea
|
||||
name="shop_offline_message"
|
||||
rows="3"
|
||||
class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3.5 py-2.5 text-xs text-white leading-relaxed focus:outline-none focus:ring-2 focus:ring-rose-400"
|
||||
>{{ setting('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 voor bestellingen!') }}</textarea>
|
||||
<p class="text-[10px] text-slate-500">Uitleg over waarom de webshop tijdelijk offline staat en wanneer klanten terug kunnen komen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Box van de Offline Banner -->
|
||||
<div class="pt-4 border-t border-slate-800 space-y-2">
|
||||
<span class="text-xs font-black uppercase tracking-wider text-slate-400 block">Voorvertoning van de Offline Banner:</span>
|
||||
<div class="rounded-2xl bg-gradient-to-b from-rose-950/80 to-slate-950 border-2 border-rose-500/60 p-6 text-center space-y-3">
|
||||
<span class="inline-block px-3 py-1 rounded-full bg-rose-600 text-white font-black text-[10px] uppercase tracking-wider">
|
||||
🔴 Webshop Tijdelijk Offline
|
||||
</span>
|
||||
<h4 class="font-heading text-xl font-black text-white">
|
||||
{{ setting('shop_offline_title', 'Aan deze webshop wordt hard gewerkt! 🛠️') }}
|
||||
</h4>
|
||||
<p class="text-xs text-slate-300 max-w-lg mx-auto">
|
||||
{{ setting('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 voor bestellingen!') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Save Action Bar -->
|
||||
<div class="sticky bottom-4 z-20 p-4 rounded-2xl bg-slate-900/95 backdrop-blur-md border border-amber-400/30 shadow-2xl flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2 text-xs text-slate-300 font-bold">
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<!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>Aan deze webshop wordt hard gewerkt! 🛠️ | Jouwdriedee.nl</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/images/logo-icon.svg">
|
||||
|
||||
<!-- Google Fonts: Fredoka & Nunito -->
|
||||
<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;800&family=Nunito:wght@400;600;700;800;900&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; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-full flex flex-col justify-between bg-gradient-to-b from-rose-950 via-slate-950 to-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-2">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="/images/logo.svg" alt="Jouwdriedee.nl" class="h-9 sm:h-11 w-auto brightness-200 invert">
|
||||
</div>
|
||||
<div class="inline-flex items-center space-x-2 px-3 py-1 rounded-full bg-rose-600/30 border border-rose-500/40 text-rose-300 text-xs font-bold">
|
||||
<span class="w-2 h-2 rounded-full bg-rose-500 animate-ping"></span>
|
||||
<span>Onderhoudsmodus</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Large Red Offline Banner / Card -->
|
||||
<main class="max-w-3xl mx-auto w-full my-auto py-8">
|
||||
<div class="rounded-3xl bg-slate-900/90 border-2 border-rose-500/80 shadow-2xl shadow-rose-900/50 p-6 sm:p-10 text-center relative overflow-hidden backdrop-blur-xl">
|
||||
|
||||
<!-- Glow effect behind content -->
|
||||
<div class="absolute -top-24 -left-24 w-72 h-72 bg-rose-600/20 rounded-full blur-3xl pointer-events-none"></div>
|
||||
<div class="absolute -bottom-24 -right-24 w-72 h-72 bg-amber-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
|
||||
<!-- Pulsing Alert Badge -->
|
||||
<div class="inline-flex items-center space-x-2 px-4 py-1.5 rounded-full bg-rose-600 text-white font-black text-xs uppercase tracking-wider shadow-lg shadow-rose-600/40 mb-6">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-white animate-pulse"></span>
|
||||
<span>Webshop Tijdelijk Offline</span>
|
||||
</div>
|
||||
|
||||
<!-- Animated 3D Printer Icon Illustration -->
|
||||
<div class="w-20 h-20 sm:w-24 sm:h-24 rounded-3xl bg-rose-500/20 border border-rose-500/40 text-rose-400 mx-auto flex items-center justify-center text-4xl sm:text-5xl mb-6 shadow-inner animate-bounce" style="animation-duration: 3s;">
|
||||
🖨️
|
||||
</div>
|
||||
|
||||
<!-- Big Main Headline -->
|
||||
<h1 class="font-heading text-2xl sm:text-4xl md:text-5xl font-black text-white tracking-tight leading-tight mb-4">
|
||||
{{ $offlineTitle ?? 'Aan deze webshop wordt hard gewerkt!' }}
|
||||
</h1>
|
||||
|
||||
<!-- Explanatory message -->
|
||||
<p class="text-slate-300 text-sm sm:text-base max-w-xl mx-auto leading-relaxed font-medium 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">
|
||||
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800/80">
|
||||
<div class="text-xl mb-1">🛠️</div>
|
||||
<h3 class="text-xs font-black text-white uppercase tracking-wider mb-1">Printstudio Update</h3>
|
||||
<p class="text-[11px] text-slate-400 leading-normal">Finetunen van laagdiktes en toevoegen van nieuwe multi-color designs.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800/80">
|
||||
<div class="text-xl mb-1">📦</div>
|
||||
<h3 class="text-xs font-black text-white uppercase tracking-wider mb-1">Lopende Orders</h3>
|
||||
<p class="text-[11px] text-slate-400 leading-normal">Reeds geplaatste bestellingen worden gewoon geprint en verzonden via PostNL.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 rounded-2xl bg-slate-950/60 border border-slate-800/80">
|
||||
<div class="text-xl mb-1">✉️</div>
|
||||
<h3 class="text-xs font-black text-white uppercase tracking-wider 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-rose-400 font-bold hover:underline">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-500 hover:text-slate-300 transition text-[11px] font-bold flex items-center space-x-1">
|
||||
<span>Beheerder inloggen</span>
|
||||
<span>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -76,6 +76,27 @@
|
||||
|
||||
<!-- Right Actions -->
|
||||
<div class="flex items-center space-x-3">
|
||||
@php
|
||||
$isShopOffline = \App\Models\Setting::get('shop_offline', '0') === '1';
|
||||
@endphp
|
||||
|
||||
<!-- Webshop Offline / Online Status Switcher -->
|
||||
<form action="{{ route('admin.toggle-offline') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
@if($isShopOffline)
|
||||
<button type="submit" class="px-3 py-1.5 rounded-xl bg-rose-600 hover:bg-rose-500 text-white text-xs font-black transition flex items-center space-x-1.5 shadow-lg shadow-rose-600/30 animate-pulse cursor-pointer" title="Klik om de webshop weer direct online te zetten">
|
||||
<span class="w-2 h-2 rounded-full bg-white"></span>
|
||||
<span>Webshop: OFFLINE</span>
|
||||
</button>
|
||||
@else
|
||||
<button type="submit" class="px-2.5 sm:px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-rose-950/40 text-slate-300 hover:text-rose-400 text-xs font-bold transition flex items-center space-x-1.5 border border-slate-700 cursor-pointer" title="Klik om de webshop direct offline te zetten voor bezoekers">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||
<span class="hidden sm:inline">Webshop: Online</span>
|
||||
<span class="text-[10px] text-slate-500 hover:text-rose-300">(Zet Offline)</span>
|
||||
</button>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<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>
|
||||
@@ -118,6 +139,24 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Prominent Offline Alert Banner -->
|
||||
@if($isShopOffline ?? (\App\Models\Setting::get('shop_offline', '0') === '1'))
|
||||
<div class="bg-gradient-to-r from-rose-700 via-rose-600 to-rose-700 text-white px-4 py-3 text-xs font-bold border-b-2 border-rose-800 shadow-xl 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.5">
|
||||
<span class="w-3 h-3 rounded-full bg-white animate-ping shrink-0"></span>
|
||||
<span class="px-2 py-0.5 rounded-md bg-white text-rose-800 font-black text-[10px] uppercase tracking-wider shrink-0">Offline Actief</span>
|
||||
<span>De webshop staat momenteel <strong>OFFLINE</strong> voor bezoekers. Bezoekers zien de grote rode onderhoudsbanner. 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-4 py-1.5 bg-white hover:bg-slate-100 text-rose-700 font-black rounded-xl text-xs transition shadow-md cursor-pointer flex items-center space-x-1.5">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
<span>Zet Webshop 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">
|
||||
|
||||
|
||||
@@ -25,6 +25,27 @@
|
||||
</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 }">
|
||||
|
||||
<!-- Admin Offline Preview Notice -->
|
||||
@if(\App\Models\Setting::get('shop_offline', '0') === '1' && auth()->check() && auth()->user()->isAdmin())
|
||||
<div class="bg-rose-600 text-white px-4 py-2 text-xs font-black flex flex-col sm:flex-row items-center justify-between sticky top-0 z-50 border-b border-rose-700 shadow-lg gap-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-white animate-pulse"></span>
|
||||
<span>⚠️ OFFLINE BEHEERDERS-PREVIEW: De webshop staat momenteel OFFLINE voor gewone bezoekers. Alleen jij als beheerder ziet deze pagina.</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="{{ route('admin.content.index') }}" class="px-2.5 py-1 rounded-lg bg-rose-700 hover:bg-rose-800 text-white text-[11px] font-bold transition">
|
||||
Naar Beheer
|
||||
</a>
|
||||
<form action="{{ route('admin.toggle-offline') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<button type="submit" class="px-3 py-1 rounded-lg bg-white text-rose-700 hover:bg-rose-50 text-[11px] font-black shadow transition">
|
||||
Zet Nu Weer Online
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 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">
|
||||
|
||||
@@ -116,6 +116,7 @@ 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');
|
||||
|
||||
@@ -257,4 +257,50 @@ class AdminContentTest extends TestCase
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
+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