232 lines
10 KiB
PHP
232 lines
10 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="vi">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>GMO-Z.com RUNSYSTEM - @yield('title', 'Dashboard')</title>
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
@stack('styles')
|
|
</head>
|
|
<body class="bg-bg-main text-text-dark font-sans antialiased" data-sidebar="expanded">
|
|
<script>
|
|
if (localStorage.getItem('sidebar-collapsed') === 'true' && window.innerWidth >= 1024) {
|
|
document.body.classList.add('sidebar-collapsed');
|
|
}
|
|
</script>
|
|
<div class="app-layout">
|
|
<!-- Mobile overlay (click outside to close) -->
|
|
<div id="sidebar-overlay" class="fixed inset-0 bg-black/30 hidden" aria-hidden="true"></div>
|
|
<!-- Sidebar Component -->
|
|
<x-sidebar />
|
|
|
|
<main class="main-content flex-1 flex flex-col min-h-screen bg-[#FAFBFF] overflow-hidden relative w-full lg:w-auto">
|
|
<!-- Header Component -->
|
|
<x-header />
|
|
|
|
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
|
@if(session('success'))
|
|
<div class="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded relative">
|
|
<span class="block sm:inline">{{ session('success') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if(session('error'))
|
|
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded relative">
|
|
<span class="block sm:inline">{{ session('error') }}</span>
|
|
</div>
|
|
@endif
|
|
|
|
@if($errors->any())
|
|
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded relative">
|
|
<ul class="list-disc pl-5">
|
|
@foreach($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
|
|
@yield('content')
|
|
</div>
|
|
</main>
|
|
</div>
|
|
@yield('modals')
|
|
@stack('modals')
|
|
<script>
|
|
window.openModal = function(id) {
|
|
const modal = document.getElementById(id);
|
|
if (!modal) return;
|
|
|
|
// 1. Prevent body scroll
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// 2. Setup initial display state
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
|
|
// 3. Trigger animations
|
|
requestAnimationFrame(() => {
|
|
modal.classList.remove('opacity-0');
|
|
modal.classList.add('opacity-100');
|
|
const panel = modal.querySelector('.modal-panel');
|
|
if (panel) {
|
|
panel.classList.remove('scale-95', 'translate-y-4');
|
|
panel.classList.add('scale-100', 'translate-y-0');
|
|
}
|
|
});
|
|
|
|
// 4. ESC to close and Trap focus
|
|
modal.setAttribute('tabindex', '-1');
|
|
modal.focus();
|
|
|
|
const escHandler = function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeModal(id);
|
|
modal.removeEventListener('keydown', escHandler);
|
|
}
|
|
};
|
|
modal.addEventListener('keydown', escHandler);
|
|
};
|
|
|
|
window.closeModal = function(id) {
|
|
const modal = document.getElementById(id);
|
|
if (!modal) return;
|
|
|
|
// 1. Trigger exit animations
|
|
modal.classList.remove('opacity-100');
|
|
modal.classList.add('opacity-0');
|
|
const panel = modal.querySelector('.modal-panel');
|
|
if (panel) {
|
|
panel.classList.remove('scale-100', 'translate-y-0');
|
|
panel.classList.add('scale-95', 'translate-y-4');
|
|
}
|
|
|
|
// 2. Wait for transition then hide
|
|
setTimeout(() => {
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
document.body.style.overflow = '';
|
|
}, 300); // 300ms matches Tailwind duration-300
|
|
};
|
|
|
|
// Mobile Sidebar Toggle Logic
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const hamburger = document.getElementById('sidebar-hamburger');
|
|
const closeBtn = document.getElementById('sidebar-close');
|
|
const overlay = document.getElementById('sidebar-overlay');
|
|
const sidebar = document.querySelector('.sidebar');
|
|
|
|
if (hamburger && sidebar && overlay && closeBtn) {
|
|
function openSidebar() {
|
|
sidebar.classList.remove('-translate-x-full');
|
|
overlay.classList.remove('hidden');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeSidebar() {
|
|
sidebar.classList.add('-translate-x-full');
|
|
overlay.classList.add('hidden');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
hamburger.addEventListener('click', function () {
|
|
if (window.innerWidth >= 1024) {
|
|
const isCollapsed = document.body.classList.toggle('sidebar-collapsed');
|
|
localStorage.setItem('sidebar-collapsed', isCollapsed ? 'true' : 'false');
|
|
} else {
|
|
openSidebar();
|
|
}
|
|
});
|
|
closeBtn.addEventListener('click', closeSidebar);
|
|
overlay.addEventListener('click', closeSidebar);
|
|
}
|
|
|
|
// Client-side flash message support
|
|
const flashSuccess = localStorage.getItem('flash_success');
|
|
if (flashSuccess) {
|
|
window.showToast(flashSuccess, 'success');
|
|
localStorage.removeItem('flash_success');
|
|
}
|
|
const flashError = localStorage.getItem('flash_error');
|
|
if (flashError) {
|
|
window.showToast(flashError, 'error');
|
|
localStorage.removeItem('flash_error');
|
|
}
|
|
|
|
// Auto prevent double submit for all standard form submissions
|
|
document.querySelectorAll('form').forEach(form => {
|
|
form.addEventListener('submit', function(e) {
|
|
const submitButtons = form.querySelectorAll('button[type="submit"], input[type="submit"]');
|
|
submitButtons.forEach(btn => {
|
|
setTimeout(() => {
|
|
btn.disabled = true;
|
|
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
}, 50);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
window.showToast = function(message, type = 'success') {
|
|
const toast = document.getElementById('global-toast');
|
|
if (!toast) return;
|
|
|
|
const msgEl = toast.querySelector('.text-sm');
|
|
if (msgEl) msgEl.textContent = message;
|
|
|
|
// Dynamically set theme styling and icon
|
|
const iconContainer = toast.querySelector('.shrink-0');
|
|
if (iconContainer) {
|
|
iconContainer.className = 'inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-lg';
|
|
let iconHtml = '';
|
|
if (type === 'success') {
|
|
iconContainer.classList.add('bg-emerald-50', 'text-emerald-500');
|
|
iconHtml = '<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>';
|
|
} else if (type === 'error') {
|
|
iconContainer.classList.add('bg-red-50', 'text-red-500');
|
|
iconHtml = '<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>';
|
|
} else if (type === 'warning') {
|
|
iconContainer.classList.add('bg-amber-50', 'text-amber-500');
|
|
iconHtml = '<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>';
|
|
} else {
|
|
iconContainer.classList.add('bg-blue-50', 'text-blue-500');
|
|
iconHtml = '<svg class="w-5.5 h-5.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>';
|
|
}
|
|
iconContainer.innerHTML = iconHtml;
|
|
}
|
|
|
|
toast.classList.remove('hidden');
|
|
toast.classList.add('flex');
|
|
|
|
requestAnimationFrame(() => {
|
|
toast.classList.remove('opacity-0', 'translate-y-[-10px]');
|
|
toast.classList.add('opacity-100', 'translate-y-0');
|
|
});
|
|
|
|
if (toast.dataset.timeoutId) {
|
|
clearTimeout(toast.dataset.timeoutId);
|
|
}
|
|
toast.dataset.timeoutId = setTimeout(() => {
|
|
window.closeToast('global-toast');
|
|
}, 3000);
|
|
};
|
|
|
|
window.closeToast = function(id) {
|
|
const toast = document.getElementById(id);
|
|
if (!toast) return;
|
|
|
|
toast.classList.remove('opacity-100', 'translate-y-0');
|
|
toast.classList.add('opacity-0', 'translate-y-[-10px]');
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('hidden');
|
|
toast.classList.remove('flex');
|
|
}, 300);
|
|
};
|
|
</script>
|
|
<x-toast id="global-toast" type="success" message="" />
|
|
@stack('scripts')
|
|
</body>
|
|
</html>
|