152 lines
6.0 KiB
PHP
152 lines
6.0 KiB
PHP
|
|
<!doctype html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||
|
|
<meta name="csrf-token" content="<?= csrf_hash() ?>" />
|
||
|
|
<meta name="csrf-header" content="<?= csrf_header() ?>" />
|
||
|
|
<title><?= esc($title ?? 'Admin Dashboard') ?> - Bapenda Garut</title>
|
||
|
|
<link rel="icon" type="image/png" href="<?= base_url('assets/images/favicon_1762970389090.png') ?>" />
|
||
|
|
<link rel="shortcut icon" type="image/png" href="<?= base_url('assets/images/favicon_1762970389090.png') ?>" />
|
||
|
|
<link rel="stylesheet" href="<?= base_url('assets/css/app.css') ?>">
|
||
|
|
<style>
|
||
|
|
/* Fix Editor.js toolbar z-index to stay below header */
|
||
|
|
.ce-toolbar,
|
||
|
|
.ce-inline-toolbar,
|
||
|
|
.ce-popover,
|
||
|
|
.ce-conversion-toolbar,
|
||
|
|
.ce-settings,
|
||
|
|
.ce-block-settings,
|
||
|
|
.ce-toolbar__plus,
|
||
|
|
.ce-toolbar__settings-btn,
|
||
|
|
.ce-popover__item,
|
||
|
|
.ce-popover__items,
|
||
|
|
.ce-settings__button,
|
||
|
|
.ce-toolbar__content,
|
||
|
|
.ce-toolbar__actions {
|
||
|
|
z-index: 10 !important;
|
||
|
|
}
|
||
|
|
header,
|
||
|
|
header[class*="sticky"],
|
||
|
|
header[class*="fixed"],
|
||
|
|
header.sticky,
|
||
|
|
header.fixed {
|
||
|
|
z-index: 99999 !important;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
|
|
<script>
|
||
|
|
// Apply dark mode immediately if stored (before Alpine loads)
|
||
|
|
(function() {
|
||
|
|
const darkMode = JSON.parse(localStorage.getItem('darkMode') || 'false');
|
||
|
|
if (darkMode) {
|
||
|
|
document.documentElement.classList.add('dark');
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
|
||
|
|
// Initialize Alpine store for dark mode
|
||
|
|
document.addEventListener('alpine:init', () => {
|
||
|
|
Alpine.store('darkMode', {
|
||
|
|
enabled: JSON.parse(localStorage.getItem('darkMode') || 'false'),
|
||
|
|
toggle() {
|
||
|
|
this.enabled = !this.enabled;
|
||
|
|
localStorage.setItem('darkMode', JSON.stringify(this.enabled));
|
||
|
|
if (this.enabled) {
|
||
|
|
document.documentElement.classList.add('dark');
|
||
|
|
} else {
|
||
|
|
document.documentElement.classList.remove('dark');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</head>
|
||
|
|
<body
|
||
|
|
x-data="{ sidebarToggle: false }"
|
||
|
|
:class="{'bg-gray-900': $store.darkMode.enabled}"
|
||
|
|
>
|
||
|
|
<!-- ===== Page Wrapper Start ===== -->
|
||
|
|
<div class="flex h-screen overflow-hidden">
|
||
|
|
<!-- ===== Sidebar Start ===== -->
|
||
|
|
<?= $this->include('admin/partials/sidebar') ?>
|
||
|
|
<!-- ===== Sidebar End ===== -->
|
||
|
|
|
||
|
|
<!-- ===== Content Area Start ===== -->
|
||
|
|
<div class="relative flex flex-col flex-1 overflow-x-hidden overflow-y-auto">
|
||
|
|
<!-- ===== Header Start ===== -->
|
||
|
|
<?= $this->include('admin/partials/navbar') ?>
|
||
|
|
<!-- ===== Header End ===== -->
|
||
|
|
|
||
|
|
<!-- ===== Main Content Start ===== -->
|
||
|
|
<main>
|
||
|
|
<div class="p-4 mx-auto max-w-7xl md:p-6">
|
||
|
|
<?php if (session()->getFlashdata('success')): ?>
|
||
|
|
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||
|
|
<p class="text-sm text-green-800"><?= esc(session()->getFlashdata('success')) ?></p>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?php if (session()->getFlashdata('error')): ?>
|
||
|
|
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||
|
|
<p class="text-sm text-red-800"><?= esc(session()->getFlashdata('error')) ?></p>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<?= $this->renderSection('content') ?>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
<!-- ===== Main Content End ===== -->
|
||
|
|
</div>
|
||
|
|
<!-- ===== Content Area End ===== -->
|
||
|
|
</div>
|
||
|
|
<!-- ===== Page Wrapper End ===== -->
|
||
|
|
|
||
|
|
<script src="<?= base_url('assets/js/app.js') ?>"></script>
|
||
|
|
<script>
|
||
|
|
// CSRF Helper for AJAX/Fetch requests
|
||
|
|
function withCsrf(options = {}) {
|
||
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||
|
|
const csrfHeader = document.querySelector('meta[name="csrf-header"]')?.getAttribute('content');
|
||
|
|
|
||
|
|
if (!csrfToken || !csrfHeader) {
|
||
|
|
console.warn('CSRF token not found');
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Merge headers
|
||
|
|
options.headers = {
|
||
|
|
...options.headers,
|
||
|
|
[csrfHeader]: csrfToken,
|
||
|
|
};
|
||
|
|
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Override fetch to automatically include CSRF token
|
||
|
|
const originalFetch = window.fetch;
|
||
|
|
window.fetch = function(url, options = {}) {
|
||
|
|
// Only add CSRF for same-origin POST/PUT/DELETE requests
|
||
|
|
if (typeof url === 'string' && (url.startsWith('/') || url.startsWith(window.location.origin))) {
|
||
|
|
const method = (options.method || 'GET').toUpperCase();
|
||
|
|
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) {
|
||
|
|
options = withCsrf(options);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return originalFetch(url, options);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Update CSRF token in meta tags after form submission
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
// Listen for form submissions and update CSRF token from response
|
||
|
|
document.addEventListener('submit', function(e) {
|
||
|
|
// After form submit, the new CSRF token will be in the response
|
||
|
|
// We'll update it when the page reloads or via AJAX response
|
||
|
|
});
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
<?= $this->renderSection('scripts') ?>
|
||
|
|
</body>
|
||
|
|
</html>
|