Files
thankcard-system/resources/views/layouts/app.blade.php
T
2026-08-01 22:54:56 +07:00

241 lines
11 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 backdrop-blur-sm hidden transition-opacity duration-200" 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>
@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');
overlay.classList.add('opacity-100');
document.body.style.overflow = 'hidden';
}
function closeSidebar() {
sidebar.classList.add('-translate-x-full');
overlay.classList.remove('opacity-100');
// after transition, hide overlay
setTimeout(() => {
overlay.classList.add('hidden');
}, 200);
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);
// ESC key to close sidebar
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !overlay.classList.contains('hidden')) {
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>