Initial commit - CMS Gov Bapenda Garut dengan EditorJS

This commit is contained in:
2026-01-05 06:47:36 +07:00
commit bd649bd5f2
634 changed files with 215640 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Audit Log
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Riwayat aktivitas sistem
</p>
</div>
</div>
<!-- Stats Card -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-1">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Log</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= number_format($total) ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-brand-100 dark:bg-brand-900/20 flex items-center justify-center">
<i class="fe fe-clipboard text-brand-600 dark:text-brand-400 text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<form method="get" action="<?= base_url('admin/audit-logs') ?>" class="flex flex-col gap-4 sm:flex-row sm:items-center">
<div class="flex-1">
<input
type="text"
name="search"
value="<?= esc($search ?? '') ?>"
placeholder="Cari aksi, user, atau IP address..."
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
<div>
<select
name="action"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua Aksi</option>
<?php foreach ($actions as $action): ?>
<option value="<?= esc($action['action']) ?>" <?= ($actionFilter === $action['action']) ? 'selected' : '' ?>>
<?= esc($action['action']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<select
name="user"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua User</option>
<?php foreach ($users as $user): ?>
<option value="<?= esc($user['id']) ?>" <?= ($userFilter == $user['id']) ? 'selected' : '' ?>>
<?= esc($user['username']) ?> (<?= esc($user['email']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-search"></i>
Cari
</button>
<?php if (!empty($search) || !empty($actionFilter) || !empty($userFilter)): ?>
<a
href="<?= base_url('admin/audit-logs') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-x"></i>
Reset
</a>
<?php endif; ?>
</form>
</div>
<!-- Audit Logs Table -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Waktu
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
User
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Aksi
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
IP Address
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
User Agent
</p>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<?php if (empty($auditLogs)): ?>
<tr>
<td colspan="5" class="px-5 py-8 text-center sm:px-6">
<p class="text-gray-500 dark:text-gray-400">Tidak ada log ditemukan.</p>
</td>
</tr>
<?php else: ?>
<?php foreach ($auditLogs as $log): ?>
<tr>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= date('d M Y H:i:s', strtotime($log['created_at'])) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-800 text-sm dark:text-white/90">
<?= esc($log['username'] ?? 'System') ?>
</p>
<?php if (!empty($log['email'])): ?>
<p class="ml-2 text-xs text-gray-500 dark:text-gray-400">
(<?= esc($log['email']) ?>)
</p>
<?php endif; ?>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-700 dark:bg-brand-500/15 dark:text-brand-500">
<?= esc($log['action']) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= esc($log['ip_address']) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-xs dark:text-gray-400 max-w-xs truncate" title="<?= esc($log['user_agent']) ?>">
<?= esc($log['user_agent']) ?>
</p>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($pager->hasMore() || $pager->getCurrentPage() > 1): ?>
<div class="flex items-center justify-between border-t border-gray-100 px-5 py-4 dark:border-gray-800 sm:px-6">
<div class="text-sm text-gray-500 dark:text-gray-400">
Menampilkan <?= count($auditLogs) ?> dari <?= $pager->getTotal() ?> log
</div>
<div class="flex items-center gap-2">
<?= $pager->links() ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,179 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Welcome Card -->
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-white/[0.03]">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-2">
Selamat Datang, <?= esc(session()->get('username') ?? 'User') ?>!
</h2>
<p class="text-gray-600 dark:text-gray-400">
Ini adalah dashboard admin Bapenda Garut. Gunakan menu di sidebar untuk navigasi.
</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-6 lg:grid-cols-4">
<!-- Total News Card -->
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-brand-100 dark:bg-brand-900/20">
<i class="fe fe-file-text text-brand-600 dark:text-brand-400 text-xl"></i>
</div>
<div class="mt-5 flex items-end justify-between">
<div>
<span class="text-sm text-gray-500 dark:text-gray-400">Total Berita</span>
<h4 class="mt-2 text-title-sm font-bold text-gray-800 dark:text-white/90">
<?= number_format($stats['news']['total']) ?>
</h4>
<div class="mt-2 flex gap-2 text-xs text-gray-500 dark:text-gray-400">
<span>Published: <?= $stats['news']['published'] ?></span>
<span>•</span>
<span>Draft: <?= $stats['news']['draft'] ?></span>
</div>
</div>
</div>
</div>
<!-- Total Pages Card -->
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-success-100 dark:bg-success-900/20">
<i class="fe fe-file text-success-600 dark:text-success-400 text-xl"></i>
</div>
<div class="mt-5 flex items-end justify-between">
<div>
<span class="text-sm text-gray-500 dark:text-gray-400">Total Halaman</span>
<h4 class="mt-2 text-title-sm font-bold text-gray-800 dark:text-white/90">
<?= number_format($stats['pages']['total']) ?>
</h4>
<div class="mt-2 flex gap-2 text-xs text-gray-500 dark:text-gray-400">
<span>Published: <?= $stats['pages']['published'] ?></span>
<span>•</span>
<span>Draft: <?= $stats['pages']['draft'] ?></span>
</div>
</div>
</div>
</div>
<!-- Total Users Card -->
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-100 dark:bg-purple-900/20">
<i class="fe fe-users text-purple-600 dark:text-purple-400 text-xl"></i>
</div>
<div class="mt-5 flex items-end justify-between">
<div>
<span class="text-sm text-gray-500 dark:text-gray-400">Total Pengguna</span>
<h4 class="mt-2 text-title-sm font-bold text-gray-800 dark:text-white/90">
<?= number_format($stats['users']['total']) ?>
</h4>
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<span>Aktif: <?= $stats['users']['active'] ?></span>
</div>
</div>
</div>
</div>
<!-- Published News Card -->
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-warning-100 dark:bg-warning-900/20">
<i class="fe fe-check-circle text-warning-600 dark:text-warning-400 text-xl"></i>
</div>
<div class="mt-5 flex items-end justify-between">
<div>
<span class="text-sm text-gray-500 dark:text-gray-400">Berita Published</span>
<h4 class="mt-2 text-title-sm font-bold text-gray-800 dark:text-white/90">
<?= number_format($stats['news']['published']) ?>
</h4>
</div>
</div>
</div>
</div>
<!-- Recent Activity Table -->
<div class="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="px-5 py-4 sm:px-6 sm:py-5">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white/90">
Aktivitas Terbaru
</h3>
</div>
<div class="max-w-full overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Waktu
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
User
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Aksi
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
IP Address
</p>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<?php if (empty($recentAuditLogs)): ?>
<tr>
<td colspan="4" class="px-5 py-8 text-center sm:px-6">
<p class="text-gray-500 dark:text-gray-400">Tidak ada aktivitas terbaru.</p>
</td>
</tr>
<?php else: ?>
<?php foreach ($recentAuditLogs as $log): ?>
<tr>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= date('d M Y H:i', strtotime($log['created_at'])) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-800 text-sm dark:text-white/90">
<?= esc($log['username'] ?? 'System') ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-700 dark:bg-brand-500/15 dark:text-brand-500">
<?= esc($log['action']) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= esc($log['ip_address']) ?>
</p>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?= $this->endSection() ?>

151
app/Views/admin/layout.php Normal file
View File

@@ -0,0 +1,151 @@
<!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>

View File

@@ -0,0 +1,170 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
<?= $news ? 'Edit Berita' : 'Tambah Berita' ?>
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
<?= $news ? 'Ubah informasi berita' : 'Tambahkan berita baru' ?>
</p>
</div>
<a
href="<?= base_url('admin/news') ?>"
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-arrow-left"></i>
Kembali
</a>
</div>
<!-- Flash Messages sudah ditangani di layout.php -->
<!-- Form -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<form
action="<?= $news ? base_url('admin/news/update/' . $news['id']) : base_url('admin/news/store') ?>"
method="post"
class="space-y-6"
>
<?= csrf_field() ?>
<!-- Title -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Judul <span class="text-error-500">*</span>
</label>
<input
type="text"
name="title"
value="<?= old('title', $news['title'] ?? '') ?>"
placeholder="Masukkan judul berita"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (isset($validation) && $validation->hasError('title')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('title')) ?></p>
<?php endif; ?>
</div>
<!-- Editor.js Container -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Konten <span class="text-error-500">*</span>
</label>
<div id="editorjs" class="min-h-[300px] rounded-lg border border-gray-300 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"></div>
<!-- Hidden inputs for Editor.js data -->
<input type="hidden" name="content" id="content" value="<?= esc($news['content'] ?? '') ?>">
<input type="hidden" name="content_json" id="content_json" value="<?= esc($news['content_json'] ?? '') ?>">
<input type="hidden" name="content_html" id="content_html" value="<?= esc($news['content_html'] ?? '') ?>">
<input type="hidden" name="excerpt" id="excerpt" value="<?= esc($news['excerpt'] ?? '') ?>">
<?php if (isset($validation) && $validation->hasError('content')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('content')) ?></p>
<?php endif; ?>
<?php if (isset($validation) && $validation->hasError('content_json')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('content_json')) ?></p>
<?php endif; ?>
</div>
<!-- Status -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Status <span class="text-error-500">*</span>
</label>
<select
name="status"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
required
>
<option value="">Pilih Status</option>
<option value="draft" <?= old('status', $news['status'] ?? '') === 'draft' ? 'selected' : '' ?>>
Draft
</option>
<option value="published" <?= old('status', $news['status'] ?? '') === 'published' ? 'selected' : '' ?>>
Published
</option>
</select>
<?php if (isset($validation) && $validation->hasError('status')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('status')) ?></p>
<?php endif; ?>
</div>
<!-- Form Actions -->
<div class="flex items-center gap-3 border-t border-gray-100 pt-6 dark:border-gray-800">
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-save"></i>
<?= $news ? 'Simpan Perubahan' : 'Simpan Berita' ?>
</button>
<a
href="<?= base_url('admin/news') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Editor.js Bundle (Built by Vite) -->
<?php
// Get manifest file to load hashed assets
$manifestPath = FCPATH . 'assets/editor/.vite/manifest.json';
$editorJsPath = base_url('assets/editor/editor.js'); // Fallback
if (file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents($manifestPath), true);
if (isset($manifest['resources/js/editor/editor.js'])) {
$editorJsPath = base_url('assets/editor/' . $manifest['resources/js/editor/editor.js']['file']);
}
}
?>
<script src="<?= $editorJsPath ?>"></script>
<script>
// CSRF & Endpoints for Editor.js
window.csrfTokenName = '<?= csrf_token() ?>';
window.csrfTokenValue = '<?= csrf_hash() ?>';
window.csrfHeaderName = '<?= csrf_header() ?>';
window.uploadEndpoint = '<?= base_url('admin/upload') ?>';
window.linkPreviewEndpoint = '<?= base_url('admin/link-preview') ?>';
window.newsId = <?= $news ? $news['id'] : 'null' ?>;
// Fix Editor.js toolbar z-index to stay below header
document.addEventListener('DOMContentLoaded', function() {
const style = document.createElement('style');
style.textContent = `
.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 {
z-index: 10 !important;
}
header,
header[class*="sticky"],
header[class*="fixed"] {
z-index: 99999 !important;
}
`;
document.head.appendChild(style);
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,319 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Berita
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Kelola berita dan artikel
</p>
</div>
<a
href="<?= base_url('admin/news/create') ?>"
class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-plus"></i>
Tambah Berita
</a>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Berita</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['total'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-brand-100 dark:bg-brand-900/20 flex items-center justify-center">
<i class="fe fe-file-text text-brand-600 dark:text-brand-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Published</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['published'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-success-100 dark:bg-success-900/20 flex items-center justify-center">
<i class="fe fe-check-circle text-success-600 dark:text-success-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Draft</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['draft'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-warning-100 dark:bg-warning-900/20 flex items-center justify-center">
<i class="fe fe-edit text-warning-600 dark:text-warning-400 text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<form method="get" action="<?= base_url('admin/news') ?>" class="flex flex-col gap-4 sm:flex-row sm:items-center">
<div class="flex-1">
<input
type="text"
name="search"
value="<?= esc($currentSearch ?? '') ?>"
placeholder="Cari berita..."
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
<div>
<select
name="status"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua Status</option>
<option value="published" <?= ($currentStatus === 'published') ? 'selected' : '' ?>>Published</option>
<option value="draft" <?= ($currentStatus === 'draft') ? 'selected' : '' ?>>Draft</option>
</select>
</div>
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-search"></i>
Cari
</button>
<?php if ($currentSearch || $currentStatus): ?>
<a
href="<?= base_url('admin/news') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-x"></i>
Reset
</a>
<?php endif; ?>
</form>
</div>
<!-- News Table -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Judul
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Status
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Dibuat Oleh
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Tanggal Dibuat
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Aksi
</p>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<?php if (empty($news)): ?>
<tr>
<td colspan="5" class="px-5 py-8 text-center sm:px-6">
<p class="text-gray-500 dark:text-gray-400">Tidak ada berita ditemukan.</p>
</td>
</tr>
<?php else: ?>
<?php foreach ($news as $item): ?>
<tr>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<div>
<p class="font-medium text-gray-800 text-sm dark:text-white/90">
<?= esc($item['title']) ?>
</p>
<span class="text-gray-500 text-xs dark:text-gray-400">
<?= esc($item['slug']) ?>
</span>
</div>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<?php if ($item['status'] === 'published'): ?>
<p class="rounded-full bg-success-50 px-2 py-0.5 text-xs font-medium text-success-700 dark:bg-success-500/15 dark:text-success-500">
Published
</p>
<?php else: ?>
<p class="rounded-full bg-warning-50 px-2 py-0.5 text-xs font-medium text-warning-700 dark:bg-warning-500/15 dark:text-warning-400">
Draft
</p>
<?php endif; ?>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= esc($item['creator_name'] ?? 'Unknown') ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= date('d M Y', strtotime($item['created_at'])) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center gap-2">
<a
href="<?= base_url('admin/news/edit/' . $item['id']) ?>"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
title="Edit"
>
<i class="fe fe-edit text-sm"></i>
<span class="hidden sm:inline">Edit</span>
</a>
<button
type="button"
onclick="confirmDelete(<?= $item['id'] ?>)"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-error-300 bg-white px-3 py-1.5 text-sm font-medium text-error-700 shadow-theme-xs hover:bg-error-50 dark:border-error-700 dark:bg-gray-800 dark:text-error-400 dark:hover:bg-error-900/20"
title="Hapus"
>
<i class="fe fe-trash-2 text-sm"></i>
<span class="hidden sm:inline">Hapus</span>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($pager->hasMore() || $pager->getCurrentPage() > 1): ?>
<div class="flex items-center justify-between border-t border-gray-100 px-5 py-4 dark:border-gray-800 sm:px-6">
<div class="text-sm text-gray-500 dark:text-gray-400">
Menampilkan <?= count($news) ?> dari <?= $pager->getTotal() ?> berita
</div>
<div class="flex items-center gap-2">
<?= $pager->links() ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50">
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900 w-full max-w-md">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-2" id="confirmModalTitle">
Hapus Berita
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4" id="confirmModalMessage">
Apakah Anda yakin ingin menghapus berita ini? Tindakan ini tidak dapat dibatalkan.
</p>
<div class="flex items-center gap-3 pt-4">
<button
type="button"
id="confirmModalButton"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-error-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-error-600"
>
Ya, Hapus
</button>
<button
type="button"
onclick="closeConfirmModal()"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</button>
</div>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" method="post" action="" style="display: none;">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />
</form>
<script>
let confirmCallback = null;
function showConfirmModal(title, message, buttonText, buttonClass, callback) {
document.getElementById('confirmModalTitle').textContent = title;
document.getElementById('confirmModalMessage').textContent = message;
const confirmBtn = document.getElementById('confirmModalButton');
confirmBtn.textContent = buttonText;
confirmBtn.className = `inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs ${buttonClass}`;
confirmCallback = callback;
document.getElementById('confirmModal').classList.remove('hidden');
document.getElementById('confirmModal').classList.add('flex');
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.add('hidden');
document.getElementById('confirmModal').classList.remove('flex');
confirmCallback = null;
}
function confirmDelete(id) {
showConfirmModal(
'Hapus Berita',
'Apakah Anda yakin ingin menghapus berita ini? Tindakan ini tidak dapat dibatalkan.',
'Ya, Hapus',
'bg-error-500 hover:bg-error-600',
function() {
const form = document.getElementById('deleteForm');
form.action = '<?= base_url('admin/news/delete/') ?>' + id;
form.submit();
}
);
}
// Handle confirm button click
document.getElementById('confirmModalButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
closeConfirmModal();
}
});
// Close modal on outside click
document.getElementById('confirmModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,228 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
<?= $page ? 'Edit Halaman' : 'Tambah Halaman' ?>
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
<?= $page ? 'Ubah informasi halaman' : 'Tambahkan halaman baru' ?>
</p>
</div>
<div class="flex items-center gap-2">
<span id="autosave-indicator" class="hidden text-sm text-gray-500 dark:text-gray-400">Disimpan otomatis</span>
<button
type="button"
id="preview-btn"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-eye"></i>
Preview
</button>
</div>
</div>
<!-- Form -->
<form
action="<?= $page ? base_url('admin/pages/update/' . $page['id']) : base_url('admin/pages/store') ?>"
method="post"
class="space-y-6"
id="page-form"
>
<?= csrf_field() ?>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Main Content Area (2/3 width) -->
<div class="lg:col-span-2 space-y-6">
<!-- Title -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Judul <span class="text-error-500">*</span>
</label>
<input
type="text"
name="title"
id="title"
value="<?= old('title', $page['title'] ?? '') ?>"
placeholder="Masukkan judul halaman"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
required
>
<?php if (isset($validation) && $validation->hasError('title')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('title')) ?></p>
<?php endif; ?>
</div>
</div>
<!-- Editor.js Container -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Konten <span class="text-error-500">*</span>
</label>
<div id="editorjs" class="min-h-[500px] rounded-lg border border-gray-300 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"></div>
<!-- Hidden inputs for Editor.js data -->
<input type="hidden" name="content" id="content" value="<?= esc($page['content'] ?? '') ?>">
<input type="hidden" name="content_json" id="content_json" value="<?= esc($page['content_json'] ?? '') ?>">
<input type="hidden" name="content_html" id="content_html" value="<?= esc($page['content_html'] ?? '') ?>">
<input type="hidden" name="excerpt" id="excerpt" value="<?= esc($page['excerpt'] ?? '') ?>">
<?php if (isset($validation) && $validation->hasError('content_json')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('content_json')) ?></p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Sidebar (1/3 width) - Document Settings -->
<div class="space-y-6">
<!-- Status -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<h3 class="mb-4 text-sm font-semibold text-gray-700 dark:text-gray-300">Pengaturan Dokumen</h3>
<div class="space-y-4">
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Status <span class="text-error-500">*</span>
</label>
<select
name="status"
id="status"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
required
>
<option value="draft" <?= old('status', $page['status'] ?? 'draft') === 'draft' ? 'selected' : '' ?>>Draft</option>
<option value="published" <?= old('status', $page['status'] ?? '') === 'published' ? 'selected' : '' ?>>Published</option>
</select>
<?php if (isset($validation) && $validation->hasError('status')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('status')) ?></p>
<?php endif; ?>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Excerpt
</label>
<textarea
name="excerpt"
id="excerpt-textarea"
rows="3"
placeholder="Ringkasan halaman (otomatis dari konten pertama)"
class="w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
><?= old('excerpt', $page['excerpt'] ?? '') ?></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Akan diisi otomatis dari paragraf pertama</p>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Featured Image URL
</label>
<input
type="url"
name="featured_image"
id="featured_image"
value="<?= old('featured_image', $page['featured_image'] ?? '') ?>"
placeholder="https://example.com/image.jpg"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<?php if (!empty($page['featured_image'] ?? '')): ?>
<div class="mt-2">
<img src="<?= esc($page['featured_image']) ?>" alt="Featured" class="h-24 w-full rounded-lg object-cover">
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Form Actions - Match grid layout -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<div class="lg:col-span-2">
<div class="flex items-center justify-end gap-3 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03]">
<a
href="<?= base_url('admin/pages') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</a>
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-save"></i>
<?= $page ? 'Simpan Perubahan' : 'Simpan Halaman' ?>
</button>
</div>
</div>
</div>
</form>
</div>
<!-- Editor.js Bundle (Built by Vite) -->
<?php
// Get manifest file to load hashed assets
$manifestPath = FCPATH . 'assets/editor/.vite/manifest.json';
$editorJsPath = base_url('assets/editor/editor.js'); // Fallback
if (file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents($manifestPath), true);
if (isset($manifest['resources/js/editor/editor.js'])) {
$editorJsPath = base_url('assets/editor/' . $manifest['resources/js/editor/editor.js']['file']);
}
}
?>
<script src="<?= $editorJsPath ?>"></script>
<script>
// CSRF & Endpoints for Editor.js
window.csrfTokenName = '<?= csrf_token() ?>';
window.csrfTokenValue = '<?= csrf_hash() ?>';
window.csrfHeaderName = '<?= csrf_header() ?>';
window.uploadEndpoint = '<?= base_url('admin/upload') ?>';
window.linkPreviewEndpoint = '<?= base_url('admin/link-preview') ?>';
window.pageId = <?= $page ? $page['id'] : 'null' ?>;
// Sync excerpt textarea with hidden input
const excerptTextarea = document.getElementById('excerpt-textarea');
const excerptInput = document.getElementById('excerpt');
if (excerptTextarea && excerptInput) {
excerptTextarea.addEventListener('input', function() {
excerptInput.value = this.value;
});
}
// Fix Editor.js toolbar z-index to stay below header
document.addEventListener('DOMContentLoaded', function() {
const style = document.createElement('style');
style.textContent = `
.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 {
z-index: 10 !important;
}
header,
header[class*="sticky"],
header[class*="fixed"] {
z-index: 99999 !important;
}
`;
document.head.appendChild(style);
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,307 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Halaman
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Kelola halaman statis
</p>
</div>
<a
href="<?= base_url('admin/pages/create') ?>"
class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-plus"></i>
Tambah Halaman
</a>
</div>
<!-- Flash Messages sudah ditangani di layout.php -->
<!-- Stats Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Halaman</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['total'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-brand-100 dark:bg-brand-900/20 flex items-center justify-center">
<i class="fe fe-file text-brand-600 dark:text-brand-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Published</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['published'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-success-100 dark:bg-success-900/20 flex items-center justify-center">
<i class="fe fe-check-circle text-success-600 dark:text-success-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Draft</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['draft'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-warning-100 dark:bg-warning-900/20 flex items-center justify-center">
<i class="fe fe-edit text-warning-600 dark:text-warning-400 text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<form method="get" action="<?= base_url('admin/pages') ?>" class="flex flex-col gap-4 sm:flex-row sm:items-center">
<div class="flex-1">
<input
type="text"
name="search"
value="<?= esc($currentSearch ?? '') ?>"
placeholder="Cari halaman..."
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
<div>
<select
name="status"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua Status</option>
<option value="published" <?= ($currentStatus === 'published') ? 'selected' : '' ?>>Published</option>
<option value="draft" <?= ($currentStatus === 'draft') ? 'selected' : '' ?>>Draft</option>
</select>
</div>
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-search"></i>
Cari
</button>
<?php if ($currentSearch || $currentStatus): ?>
<a
href="<?= base_url('admin/pages') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-x"></i>
Reset
</a>
<?php endif; ?>
</form>
</div>
<!-- Pages Table -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Judul
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Status
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Tanggal Dibuat
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Aksi
</p>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<?php if (empty($pages)): ?>
<tr>
<td colspan="4" class="px-5 py-8 text-center sm:px-6">
<p class="text-gray-500 dark:text-gray-400">Tidak ada halaman ditemukan.</p>
</td>
</tr>
<?php else: ?>
<?php foreach ($pages as $item): ?>
<tr>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<div>
<p class="font-medium text-gray-800 text-sm dark:text-white/90">
<?= esc($item['title']) ?>
</p>
<span class="text-gray-500 text-xs dark:text-gray-400">
<?= esc($item['slug']) ?>
</span>
</div>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<?php if ($item['status'] === 'published'): ?>
<p class="rounded-full bg-success-50 px-2 py-0.5 text-xs font-medium text-success-700 dark:bg-success-500/15 dark:text-success-500">
Published
</p>
<?php else: ?>
<p class="rounded-full bg-warning-50 px-2 py-0.5 text-xs font-medium text-warning-700 dark:bg-warning-500/15 dark:text-warning-400">
Draft
</p>
<?php endif; ?>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= date('d M Y', strtotime($item['created_at'])) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center gap-2">
<a
href="<?= base_url('admin/pages/edit/' . $item['id']) ?>"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
title="Edit"
>
<i class="fe fe-edit text-sm"></i>
<span class="hidden sm:inline">Edit</span>
</a>
<button
type="button"
onclick="confirmDelete(<?= $item['id'] ?>)"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-error-300 bg-white px-3 py-1.5 text-sm font-medium text-error-700 shadow-theme-xs hover:bg-error-50 dark:border-error-700 dark:bg-gray-800 dark:text-error-400 dark:hover:bg-error-900/20"
title="Hapus"
>
<i class="fe fe-trash-2 text-sm"></i>
<span class="hidden sm:inline">Hapus</span>
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($pager->hasMore() || $pager->getCurrentPage() > 1): ?>
<div class="flex items-center justify-between border-t border-gray-100 px-5 py-4 dark:border-gray-800 sm:px-6">
<div class="text-sm text-gray-500 dark:text-gray-400">
Menampilkan <?= count($pages) ?> dari <?= $pager->getTotal() ?> halaman
</div>
<div class="flex items-center gap-2">
<?= $pager->links() ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50">
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900 w-full max-w-md">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-2" id="confirmModalTitle">
Hapus Halaman
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4" id="confirmModalMessage">
Apakah Anda yakin ingin menghapus halaman ini? Tindakan ini tidak dapat dibatalkan.
</p>
<div class="flex items-center gap-3 pt-4">
<button
type="button"
id="confirmModalButton"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-error-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-error-600"
>
Ya, Hapus
</button>
<button
type="button"
onclick="closeConfirmModal()"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</button>
</div>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" method="post" action="" style="display: none;">
<input type="hidden" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />
</form>
<script>
let confirmCallback = null;
function showConfirmModal(title, message, buttonText, buttonClass, callback) {
document.getElementById('confirmModalTitle').textContent = title;
document.getElementById('confirmModalMessage').textContent = message;
const confirmBtn = document.getElementById('confirmModalButton');
confirmBtn.textContent = buttonText;
confirmBtn.className = `inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs ${buttonClass}`;
confirmCallback = callback;
document.getElementById('confirmModal').classList.remove('hidden');
document.getElementById('confirmModal').classList.add('flex');
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.add('hidden');
document.getElementById('confirmModal').classList.remove('flex');
confirmCallback = null;
}
function confirmDelete(id) {
showConfirmModal(
'Hapus Halaman',
'Apakah Anda yakin ingin menghapus halaman ini? Tindakan ini tidak dapat dibatalkan.',
'Ya, Hapus',
'bg-error-500 hover:bg-error-600',
function() {
const form = document.getElementById('deleteForm');
form.action = '<?= base_url('admin/pages/delete/') ?>' + id;
form.submit();
}
);
}
// Handle confirm button click
document.getElementById('confirmModalButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
closeConfirmModal();
}
});
// Close modal on outside click
document.getElementById('confirmModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,111 @@
<header
x-data="{menuToggle: false}"
class="sticky top-0 z-99999 flex w-full border-gray-200 bg-white lg:border-b dark:border-gray-800 dark:bg-gray-900"
>
<div class="flex grow flex-col items-center justify-between lg:flex-row lg:px-6">
<div class="flex w-full items-center justify-between gap-2 border-b border-gray-200 px-3 py-3 sm:gap-4 lg:justify-normal lg:border-b-0 lg:px-0 lg:py-4 dark:border-gray-800">
<!-- Hamburger Toggle BTN -->
<button
:class="sidebarToggle ? 'lg:bg-transparent dark:lg:bg-transparent bg-gray-100 dark:bg-gray-800' : ''"
class="z-99999 flex h-10 w-10 items-center justify-center rounded-lg border-gray-200 text-gray-500 lg:h-11 lg:w-11 lg:border dark:border-gray-800 dark:text-gray-400"
@click.stop="sidebarToggle = !sidebarToggle"
>
<i class="fe fe-menu hidden lg:block text-base"></i>
<i :class="sidebarToggle ? 'hidden' : 'block lg:hidden'" class="fe fe-menu block lg:hidden text-lg"></i>
<i :class="sidebarToggle ? 'block lg:hidden' : 'hidden'" class="fe fe-x block lg:hidden text-lg"></i>
</button>
<!-- Hamburger Toggle BTN -->
<!-- Page Title -->
<div class="lg:hidden">
<h2 class="text-lg font-semibold text-gray-800 dark:text-white"><?= esc($title ?? 'Dashboard') ?></h2>
</div>
<!-- Search -->
<div class="hidden lg:block">
<form>
<div class="relative">
<span class="absolute top-1/2 left-4 -translate-y-1/2">
<i class="fe fe-search text-gray-500 dark:text-gray-400 text-lg"></i>
</span>
<input
type="text"
placeholder="Search..."
class="h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pr-4 pl-12 text-sm text-gray-800 placeholder:text-gray-400 focus:border-primary-300 focus:ring-3 focus:ring-primary-500/10 focus:outline-none xl:w-[430px] dark:border-gray-800 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30"
/>
</div>
</form>
</div>
</div>
<div :class="menuToggle ? 'flex' : 'hidden'" class="shadow-md w-full items-center justify-between gap-4 px-5 py-4 lg:flex lg:justify-end lg:px-0 lg:shadow-none">
<div class="flex items-center gap-2">
<!-- Dark Mode Toggler -->
<button
@click="$store.darkMode.toggle()"
class="relative flex h-11 w-11 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
>
<i class="fe fe-sun hidden dark:block text-lg"></i>
<i class="fe fe-moon dark:hidden text-lg"></i>
</button>
<!-- Dark Mode Toggler -->
<!-- Notification Menu Area -->
<div class="relative" x-data="{ dropdownOpen: false }" @click.outside="dropdownOpen = false">
<button
class="relative flex h-11 w-11 items-center justify-center rounded-full border border-gray-200 bg-white text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
@click.prevent="dropdownOpen = !dropdownOpen"
>
<i class="fe fe-bell text-lg"></i>
</button>
</div>
<!-- Notification Menu Area -->
</div>
<!-- User Area -->
<div class="relative" x-data="{ dropdownOpen: false }" @click.outside="dropdownOpen = false">
<a
class="flex items-center text-gray-700 dark:text-gray-400"
href="#"
@click.prevent="dropdownOpen = !dropdownOpen"
>
<span class="mr-3 h-11 w-11 overflow-hidden rounded-full">
<img src="<?= base_url('assets/images/user/owner.jpg') ?>" alt="User" class="h-full w-full object-cover" />
</span>
<span class="text-sm mr-1 block font-medium dark:text-white"><?= esc(session()->get('username') ?? 'User') ?></span>
<i :class="dropdownOpen && 'rotate-180'" class="fe fe-chevron-down text-gray-500 dark:text-gray-400 text-sm transition-transform"></i>
</a>
<!-- Dropdown Start -->
<div
x-show="dropdownOpen"
class="shadow-lg absolute right-0 mt-[17px] flex w-[260px] flex-col rounded-2xl border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900"
>
<div>
<span class="text-sm block font-medium text-gray-700 dark:text-gray-400">
<?= esc(session()->get('username') ?? 'User') ?>
</span>
<span class="text-xs mt-0.5 block text-gray-500 dark:text-gray-400">
<?= esc(session()->get('email') ?? 'user@example.com') ?>
</span>
</div>
<ul class="flex flex-col gap-1 border-b border-gray-200 pt-4 pb-3 dark:border-gray-800">
<li>
<a href="<?= base_url('admin/profile') ?>" class="group text-sm flex items-center gap-3 rounded-lg px-3 py-2 font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/5">
<i class="fe fe-user text-gray-500 group-hover:text-gray-700 dark:text-gray-400"></i>
Edit profile
</a>
</li>
</ul>
<a href="<?= base_url('auth/logout') ?>" class="group text-sm mt-3 flex items-center gap-3 rounded-lg px-3 py-2 font-medium text-red-600 hover:bg-gray-100 dark:text-red-400 dark:hover:bg-white/5">
<i class="fe fe-log-out text-red-500 group-hover:text-red-700 dark:text-red-400"></i>
Sign out
</a>
</div>
<!-- Dropdown End -->
</div>
<!-- User Area -->
</div>
</div>
</header>

View File

@@ -0,0 +1,177 @@
<aside
:class="sidebarToggle ? 'translate-x-0 lg:w-[90px]' : '-translate-x-full'"
class="sidebar fixed left-0 top-0 z-9999 flex h-screen w-[290px] flex-col overflow-y-hidden border-r border-gray-200 bg-white px-5 dark:border-gray-800 dark:bg-gray-900 lg:static lg:translate-x-0"
>
<!-- SIDEBAR HEADER -->
<div
:class="sidebarToggle ? 'justify-center' : 'justify-between'"
class="flex items-center gap-2 pt-8 sidebar-header pb-7"
>
<a href="<?= base_url('admin/dashboard') ?>" class="flex items-center gap-3">
<span class="logo" :class="sidebarToggle ? 'hidden' : ''">
<img class="h-10 w-auto" src="<?= base_url('assets/images/logo/b_logo_1757803697487.png') ?>" alt="Logo Bapenda Garut" />
</span>
<img
class="logo-icon h-10 w-10 object-contain"
:class="sidebarToggle ? 'lg:block' : 'hidden'"
src="<?= base_url('assets/images/logo/b_logo_1757803697487.png') ?>"
alt="Logo"
/>
<?php
// Get site name from settings
$settingsModel = new \App\Models\SettingsModel();
$siteName = $settingsModel->getSetting('site_name', 'Bapenda Garut');
?>
<span class="site-name text-2xl font-semibold text-gray-800 dark:text-white" :class="sidebarToggle ? 'lg:hidden' : ''">
<?= esc($siteName) ?>
</span>
</a>
</div>
<!-- SIDEBAR HEADER -->
<div class="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
<!-- Sidebar Menu -->
<nav>
<?php
// Get current URI segment
$uri = service('uri');
$segment1 = $uri->getSegment(1) ?? '';
$segment2 = $uri->getSegment(2) ?? '';
// Determine active menu based on URI
$activeMenu = '';
if ($segment1 === 'admin') {
if (empty($segment2) || $segment2 === 'dashboard') {
$activeMenu = 'dashboard';
} elseif ($segment2 === 'news') {
$activeMenu = 'news';
} elseif ($segment2 === 'pages') {
$activeMenu = 'pages';
} elseif ($segment2 === 'users') {
$activeMenu = 'users';
} elseif ($segment2 === 'audit-logs') {
$activeMenu = 'audit-logs';
} elseif ($segment2 === 'settings') {
$activeMenu = 'settings';
}
}
// Helper function to get active class
$getActiveClass = function($menu) use ($activeMenu) {
return $activeMenu === $menu
? 'bg-primary-50 text-primary-600 dark:bg-white/5 dark:text-primary-400'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5';
};
?>
<!-- Menu Group -->
<div>
<h3 class="mb-4 text-xs uppercase leading-[20px] text-gray-400">
<span class="menu-group-title" :class="sidebarToggle ? 'lg:hidden' : ''">
MENU
</span>
</h3>
<ul class="flex flex-col gap-0.5 mb-6">
<!-- Menu Item Dashboard -->
<li>
<a
href="<?= base_url('admin/dashboard') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('dashboard') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-home text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Dashboard
</span>
</a>
</li>
<!-- Menu Item Dashboard -->
<!-- Menu Item News -->
<li>
<a
href="<?= base_url('admin/news') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('news') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-file-text text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Berita
</span>
</a>
</li>
<!-- Menu Item News -->
<!-- Menu Item Pages -->
<li>
<a
href="<?= base_url('admin/pages') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('pages') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-file text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Halaman
</span>
</a>
</li>
<!-- Menu Item Pages -->
<?php if (session()->get('role') === 'admin'): ?>
<!-- Menu Item Users -->
<li>
<a
href="<?= base_url('admin/users') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('users') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-users text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Pengguna
</span>
</a>
</li>
<!-- Menu Item Users -->
<!-- Menu Item Audit Logs -->
<li>
<a
href="<?= base_url('admin/audit-logs') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('audit-logs') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-clipboard text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Audit Log
</span>
</a>
</li>
<!-- Menu Item Audit Logs -->
<!-- Menu Item Settings -->
<li>
<a
href="<?= base_url('admin/settings') ?>"
class="menu-item group flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium duration-300 <?= $getActiveClass('settings') ?>"
>
<span class="flex items-center justify-center w-6 h-6">
<i class="fe fe-settings text-xl"></i>
</span>
<span class="menu-item-text" :class="sidebarToggle ? 'lg:hidden' : ''">
Pengaturan
</span>
</a>
</li>
<!-- Menu Item Settings -->
<?php endif; ?>
</ul>
</div>
</nav>
<!-- Sidebar Menu -->
</div>
</aside>

View File

@@ -0,0 +1,155 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Edit Profile
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Ubah informasi profile Anda
</p>
</div>
<a
href="<?= base_url('admin/dashboard') ?>"
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-arrow-left"></i>
Kembali
</a>
</div>
<!-- Flash Messages -->
<?php if (session()->getFlashdata('error')): ?>
<div class="rounded-lg border border-error-200 bg-error-50 p-4 dark:border-error-800 dark:bg-error-900/20">
<p class="text-sm text-error-800 dark:text-error-400">
<?= esc(session()->getFlashdata('error')) ?>
</p>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="rounded-lg border border-success-200 bg-success-50 p-4 dark:border-success-800 dark:bg-success-900/20">
<p class="text-sm text-success-800 dark:text-success-400">
<?= esc(session()->getFlashdata('success')) ?>
</p>
</div>
<?php endif; ?>
<!-- Form -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<form
action="<?= base_url('admin/profile/update') ?>"
method="post"
class="space-y-6"
>
<?= csrf_field() ?>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Username -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Username <span class="text-error-500">*</span>
</label>
<input
type="text"
name="username"
value="<?= old('username', $user['username'] ?? '') ?>"
placeholder="Masukkan username"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (session()->getFlashdata('errors') && isset(session()->getFlashdata('errors')['username'])): ?>
<p class="mt-1 text-sm text-error-600"><?= esc(session()->getFlashdata('errors')['username']) ?></p>
<?php endif; ?>
</div>
<!-- Email -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Email <span class="text-error-500">*</span>
</label>
<input
type="email"
name="email"
value="<?= old('email', $user['email'] ?? '') ?>"
placeholder="Masukkan email"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (session()->getFlashdata('errors') && isset(session()->getFlashdata('errors')['email'])): ?>
<p class="mt-1 text-sm text-error-600"><?= esc(session()->getFlashdata('errors')['email']) ?></p>
<?php endif; ?>
</div>
<!-- Password (optional) -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Password Baru
</label>
<input
type="password"
name="password"
placeholder="Kosongkan jika tidak ingin mengubah password"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
minlength="6"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Minimal 6 karakter. Kosongkan jika tidak ingin mengubah password.
</p>
</div>
<!-- Phone Number -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Nomor Telepon
</label>
<input
type="text"
name="phone_number"
value="<?= old('phone_number', $user['phone_number'] ?? '') ?>"
placeholder="Masukkan nomor telepon"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
<!-- Telegram ID -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Telegram ID
</label>
<input
type="number"
name="telegram_id"
value="<?= old('telegram_id', $user['telegram_id'] ?? '') ?>"
placeholder="Masukkan Telegram ID"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
</div>
<!-- Form Actions -->
<div class="flex items-center gap-3 border-t border-gray-100 pt-6 dark:border-gray-800">
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-save"></i>
Simpan Perubahan
</button>
<a
href="<?= base_url('admin/dashboard') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</a>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,79 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Pengaturan
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Kelola pengaturan sistem
</p>
</div>
</div>
<!-- Form -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<form
action="<?= base_url('admin/settings/update') ?>"
method="post"
class="space-y-6"
>
<?= csrf_field() ?>
<!-- Site Name -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Nama Situs <span class="text-error-500">*</span>
</label>
<input
type="text"
name="site_name"
value="<?= esc(old('site_name', $settings['site_name']['value'] ?? 'Bapenda Garut')) ?>"
placeholder="Masukkan nama situs"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (session()->getFlashdata('errors') && isset(session()->getFlashdata('errors')['site_name'])): ?>
<p class="mt-1 text-sm text-error-600"><?= esc(session()->getFlashdata('errors')['site_name']) ?></p>
<?php endif; ?>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Nama situs akan ditampilkan di sidebar dan judul halaman.
</p>
</div>
<!-- Site Description -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Deskripsi Situs
</label>
<textarea
name="site_description"
rows="3"
placeholder="Masukkan deskripsi situs"
class="w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
><?= esc(old('site_description', $settings['site_description']['value'] ?? '')) ?></textarea>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Deskripsi singkat tentang situs (opsional).
</p>
</div>
<!-- Form Actions -->
<div class="flex items-center gap-3 border-t border-gray-100 pt-6 dark:border-gray-800">
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-save"></i>
Simpan Pengaturan
</button>
</div>
</form>
</div>
</div>
</div>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,224 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
<?= $user ? 'Edit Pengguna' : 'Tambah Pengguna' ?>
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
<?= $user ? 'Ubah informasi pengguna' : 'Tambahkan pengguna baru' ?>
</p>
</div>
<a
href="<?= base_url('admin/users') ?>"
class="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-arrow-left"></i>
Kembali
</a>
</div>
<!-- Flash Messages -->
<?php if (session()->getFlashdata('error')): ?>
<div class="rounded-lg border border-error-200 bg-error-50 p-4 dark:border-error-800 dark:bg-error-900/20">
<p class="text-sm text-error-800 dark:text-error-400">
<?= esc(session()->getFlashdata('error')) ?>
</p>
</div>
<?php endif; ?>
<!-- Form -->
<div class="rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="p-5 sm:p-6">
<form
action="<?= $user ? base_url('admin/users/update/' . $user['id']) : base_url('admin/users/store') ?>"
method="post"
class="space-y-6"
>
<?= csrf_field() ?>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Username -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Username <span class="text-error-500">*</span>
</label>
<input
type="text"
name="username"
value="<?= old('username', $user['username'] ?? '') ?>"
placeholder="Masukkan username"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (isset($validation) && $validation->hasError('username')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('username')) ?></p>
<?php endif; ?>
</div>
<!-- Email -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Email <span class="text-error-500">*</span>
</label>
<input
type="email"
name="email"
value="<?= old('email', $user['email'] ?? '') ?>"
placeholder="Masukkan email"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<?php if (isset($validation) && $validation->hasError('email')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('email')) ?></p>
<?php endif; ?>
</div>
<!-- Password (only for create) -->
<?php if (!$user): ?>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Password <span class="text-error-500">*</span>
</label>
<input
type="password"
name="password"
placeholder="Masukkan password (min 6 karakter)"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
minlength="6"
/>
<?php if (isset($validation) && $validation->hasError('password')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('password')) ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Role -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Role <span class="text-error-500">*</span>
</label>
<select
name="role_id"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
required
>
<option value="">Pilih Role</option>
<?php foreach ($roles as $role): ?>
<option value="<?= $role['id'] ?>" <?= old('role_id', $user['role_id'] ?? '') == $role['id'] ? 'selected' : '' ?>>
<?= esc(ucfirst($role['name'])) ?>
</option>
<?php endforeach; ?>
</select>
<?php if (isset($validation) && $validation->hasError('role_id')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('role_id')) ?></p>
<?php endif; ?>
</div>
<!-- Phone Number -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Nomor Telepon
</label>
<input
type="text"
name="phone_number"
value="<?= old('phone_number', $user['phone_number'] ?? '') ?>"
placeholder="Masukkan nomor telepon"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
<?php if (isset($validation) && $validation->hasError('phone_number')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('phone_number')) ?></p>
<?php endif; ?>
</div>
<!-- Telegram ID -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Telegram ID
</label>
<input
type="number"
name="telegram_id"
value="<?= old('telegram_id', $user['telegram_id'] ?? '') ?>"
placeholder="Masukkan Telegram ID"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
<?php if (isset($validation) && $validation->hasError('telegram_id')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('telegram_id')) ?></p>
<?php endif; ?>
</div>
<!-- Active Status -->
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Status
</label>
<div class="flex items-center gap-4">
<label class="flex items-center cursor-pointer">
<input
type="checkbox"
name="is_active"
value="1"
<?= old('is_active', $user['is_active'] ?? 1) ? 'checked' : '' ?>
class="sr-only"
/>
<div class="relative">
<div class="block h-8 w-14 rounded-full <?= old('is_active', $user['is_active'] ?? 1) ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-700' ?> transition-colors"></div>
<div class="absolute left-1 top-1 h-6 w-6 rounded-full bg-white transition-transform <?= old('is_active', $user['is_active'] ?? 1) ? 'translate-x-6' : '' ?>"></div>
</div>
<span class="ml-3 text-sm text-gray-700 dark:text-gray-400">
<?= old('is_active', $user['is_active'] ?? 1) ? 'Aktif' : 'Tidak Aktif' ?>
</span>
</label>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="flex items-center gap-3 border-t border-gray-100 pt-6 dark:border-gray-800">
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-save"></i>
<?= $user ? 'Simpan Perubahan' : 'Simpan Pengguna' ?>
</button>
<a
href="<?= base_url('admin/users') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</a>
</div>
</form>
</div>
</div>
</div>
<script>
// Toggle switch functionality
document.querySelector('input[name="is_active"]')?.addEventListener('change', function() {
const toggle = this.closest('label').querySelector('.block');
const circle = this.closest('label').querySelector('.absolute');
const text = this.closest('label').querySelector('span');
if (this.checked) {
toggle.classList.add('bg-brand-500');
toggle.classList.remove('bg-gray-300', 'dark:bg-gray-700');
circle.classList.add('translate-x-6');
text.textContent = 'Aktif';
} else {
toggle.classList.remove('bg-brand-500');
toggle.classList.add('bg-gray-300', 'dark:bg-gray-700');
circle.classList.remove('translate-x-6');
text.textContent = 'Tidak Aktif';
}
});
</script>
<?= $this->endSection() ?>

View File

@@ -0,0 +1,460 @@
<?= $this->extend('admin/layout') ?>
<?= $this->section('content') ?>
<div class="space-y-5 sm:space-y-6">
<!-- Page Header -->
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 class="text-2xl font-semibold text-gray-800 dark:text-white/90">
Pengguna
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400">
Kelola pengguna sistem
</p>
</div>
<a
href="<?= base_url('admin/users/create') ?>"
class="inline-flex items-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-plus"></i>
Tambah Pengguna
</a>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Total Pengguna</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['total'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-brand-100 dark:bg-brand-900/20 flex items-center justify-center">
<i class="fe fe-users text-brand-600 dark:text-brand-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Aktif</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['active'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-success-100 dark:bg-success-900/20 flex items-center justify-center">
<i class="fe fe-check-circle text-success-600 dark:text-success-400 text-xl"></i>
</div>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Tidak Aktif</p>
<p class="text-2xl font-bold text-gray-800 dark:text-white"><?= $stats['inactive'] ?></p>
</div>
<div class="w-12 h-12 rounded-full bg-error-100 dark:bg-error-900/20 flex items-center justify-center">
<i class="fe fe-x-circle text-error-600 dark:text-error-400 text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-white/[0.03]">
<form method="get" action="<?= base_url('admin/users') ?>" class="flex flex-col gap-4 sm:flex-row sm:items-center">
<div class="flex-1">
<input
type="text"
name="search"
value="<?= esc($currentSearch ?? '') ?>"
placeholder="Cari pengguna..."
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/>
</div>
<div>
<select
name="role"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua Role</option>
<?php foreach ($roles as $role): ?>
<option value="<?= esc($role['name']) ?>" <?= ($currentRole === $role['name']) ? 'selected' : '' ?>>
<?= esc(ucfirst($role['name'])) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<select
name="status"
class="h-11 rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
>
<option value="">Semua Status</option>
<option value="1" <?= ($currentStatus === '1') ? 'selected' : '' ?>>Aktif</option>
<option value="0" <?= ($currentStatus === '0') ? 'selected' : '' ?>>Tidak Aktif</option>
</select>
</div>
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
<i class="fe fe-search"></i>
Cari
</button>
<?php if ($currentSearch || $currentRole || $currentStatus !== null): ?>
<a
href="<?= base_url('admin/users') ?>"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
<i class="fe fe-x"></i>
Reset
</a>
<?php endif; ?>
</form>
</div>
<!-- Users Table -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto">
<table class="min-w-full">
<thead>
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Username
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Email
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Role
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Status
</p>
</div>
</th>
<th class="px-5 py-3 sm:px-6">
<div class="flex items-center">
<p class="font-medium text-gray-500 text-xs dark:text-gray-400">
Aksi
</p>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<?php if (empty($users)): ?>
<tr>
<td colspan="5" class="px-5 py-8 text-center sm:px-6">
<p class="text-gray-500 dark:text-gray-400">Tidak ada pengguna ditemukan.</p>
</td>
</tr>
<?php else: ?>
<?php foreach ($users as $item): ?>
<tr>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<div>
<p class="font-medium text-gray-800 text-sm dark:text-white/90">
<?= esc($item['username']) ?>
</p>
<?php if (!empty($item['phone_number'])): ?>
<span class="text-gray-500 text-xs dark:text-gray-400">
<?= esc($item['phone_number']) ?>
</span>
<?php endif; ?>
</div>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="text-gray-500 text-sm dark:text-gray-400">
<?= esc($item['email']) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<p class="rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-700 dark:bg-brand-500/15 dark:text-brand-500">
<?= esc(ucfirst($item['role_name'] ?? 'Unknown')) ?>
</p>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center">
<?php if ($item['is_active']): ?>
<p class="rounded-full bg-success-50 px-2 py-0.5 text-xs font-medium text-success-700 dark:bg-success-500/15 dark:text-success-500">
Aktif
</p>
<?php else: ?>
<p class="rounded-full bg-error-50 px-2 py-0.5 text-xs font-medium text-error-700 dark:bg-error-500/15 dark:text-error-500">
Tidak Aktif
</p>
<?php endif; ?>
</div>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center gap-2">
<a
href="<?= base_url('admin/users/edit/' . $item['id']) ?>"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
title="Edit"
>
<i class="fe fe-edit text-sm"></i>
<span class="hidden sm:inline">Edit</span>
</a>
<button
type="button"
onclick="showResetPasswordModal(<?= $item['id'] ?>, '<?= esc($item['username']) ?>')"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-warning-300 bg-white px-3 py-1.5 text-sm font-medium text-warning-700 shadow-theme-xs hover:bg-warning-50 dark:border-warning-700 dark:bg-gray-800 dark:text-warning-400 dark:hover:bg-warning-900/20"
title="Reset Password"
>
<i class="fe fe-lock text-sm"></i>
<span class="hidden sm:inline">Reset</span>
</button>
<?php if ($item['id'] != session()->get('user_id')): ?>
<button
type="button"
onclick="toggleActive(<?= $item['id'] ?>, <?= $item['is_active'] ? 0 : 1 ?>)"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border <?= $item['is_active'] ? 'border-error-300 text-error-700 hover:bg-error-50' : 'border-success-300 text-success-700 hover:bg-success-50' ?> bg-white px-3 py-1.5 text-sm font-medium shadow-theme-xs dark:bg-gray-800 dark:hover:bg-white/[0.03]"
title="<?= $item['is_active'] ? 'Nonaktifkan' : 'Aktifkan' ?>"
>
<i class="fe <?= $item['is_active'] ? 'fe-x-circle' : 'fe-check-circle' ?> text-sm"></i>
<span class="hidden sm:inline"><?= $item['is_active'] ? 'Nonaktif' : 'Aktif' ?></span>
</button>
<button
type="button"
onclick="deleteUser(<?= $item['id'] ?>, '<?= esc($item['username']) ?>')"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-error-300 bg-white px-3 py-1.5 text-sm font-medium text-error-700 shadow-theme-xs hover:bg-error-50 dark:border-error-700 dark:bg-gray-800 dark:text-error-400 dark:hover:bg-error-900/20"
title="Hapus"
>
<i class="fe fe-trash-2 text-sm"></i>
<span class="hidden sm:inline">Hapus</span>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($pager->hasMore() || $pager->getCurrentPage() > 1): ?>
<div class="flex items-center justify-between border-t border-gray-100 px-5 py-4 dark:border-gray-800 sm:px-6">
<div class="text-sm text-gray-500 dark:text-gray-400">
Menampilkan <?= count($users) ?> dari <?= $pager->getTotal() ?> pengguna
</div>
<div class="flex items-center gap-2">
<?= $pager->links() ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50">
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900 w-full max-w-md">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-2" id="confirmModalTitle">
Konfirmasi
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4" id="confirmModalMessage">
Apakah Anda yakin?
</p>
<div class="flex items-center gap-3 pt-4">
<button
type="button"
id="confirmModalButton"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
Ya, Lanjutkan
</button>
<button
type="button"
onclick="closeConfirmModal()"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</button>
</div>
</div>
</div>
<!-- Reset Password Modal -->
<div id="resetPasswordModal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50">
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-800 dark:bg-gray-900 w-full max-w-md">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">
Reset Password
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
Reset password untuk: <span id="resetUsername" class="font-medium"></span>
</p>
<form id="resetPasswordForm" method="post" action="" class="space-y-4">
<?= csrf_field() ?>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Password Baru <span class="text-error-500">*</span>
</label>
<input
type="password"
name="new_password"
placeholder="Masukkan password baru"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
minlength="6"
/>
</div>
<div>
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Konfirmasi Password <span class="text-error-500">*</span>
</label>
<input
type="password"
name="confirm_password"
placeholder="Konfirmasi password baru"
class="h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
minlength="6"
/>
</div>
<div class="flex items-center gap-3 pt-4">
<button
type="submit"
class="inline-flex items-center justify-center gap-2 rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:bg-brand-600"
>
Reset Password
</button>
<button
type="button"
onclick="closeResetPasswordModal()"
class="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03]"
>
Batal
</button>
</div>
</form>
</div>
</div>
<script>
function showResetPasswordModal(userId, username) {
document.getElementById('resetUsername').textContent = username;
document.getElementById('resetPasswordForm').action = '<?= base_url('admin/users/reset-password/') ?>' + userId;
document.getElementById('resetPasswordModal').classList.remove('hidden');
document.getElementById('resetPasswordModal').classList.add('flex');
}
function closeResetPasswordModal() {
document.getElementById('resetPasswordModal').classList.add('hidden');
document.getElementById('resetPasswordModal').classList.remove('flex');
document.getElementById('resetPasswordForm').reset();
}
let confirmCallback = null;
function showConfirmModal(title, message, buttonText, buttonClass, callback) {
document.getElementById('confirmModalTitle').textContent = title;
document.getElementById('confirmModalMessage').textContent = message;
const confirmBtn = document.getElementById('confirmModalButton');
confirmBtn.textContent = buttonText;
confirmBtn.className = `inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm font-medium text-white shadow-theme-xs hover:opacity-90 ${buttonClass}`;
confirmCallback = callback;
document.getElementById('confirmModal').classList.remove('hidden');
document.getElementById('confirmModal').classList.add('flex');
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.add('hidden');
document.getElementById('confirmModal').classList.remove('flex');
confirmCallback = null;
}
function toggleActive(userId, newStatus) {
const action = newStatus ? 'mengaktifkan' : 'menonaktifkan';
const actionText = newStatus ? 'mengaktifkan' : 'menonaktifkan';
const buttonClass = newStatus ? 'bg-success-500 hover:bg-success-600' : 'bg-warning-500 hover:bg-warning-600';
showConfirmModal(
'Konfirmasi',
`Apakah Anda yakin ingin ${actionText} pengguna ini?`,
'Ya, Lanjutkan',
buttonClass,
function() {
const form = document.createElement('form');
form.method = 'POST';
form.action = '<?= base_url('admin/users/toggle-active/') ?>' + userId;
const csrf = document.createElement('input');
csrf.type = 'hidden';
csrf.name = '<?= csrf_token() ?>';
csrf.value = '<?= csrf_hash() ?>';
form.appendChild(csrf);
document.body.appendChild(form);
form.submit();
}
);
}
function deleteUser(userId, username) {
showConfirmModal(
'Hapus Pengguna',
`Apakah Anda yakin ingin menghapus pengguna "${username}"? Tindakan ini tidak dapat dibatalkan.`,
'Ya, Hapus',
'bg-error-500 hover:bg-error-600',
function() {
const form = document.createElement('form');
form.method = 'POST';
form.action = '<?= base_url('admin/users/delete/') ?>' + userId;
const csrf = document.createElement('input');
csrf.type = 'hidden';
csrf.name = '<?= csrf_token() ?>';
csrf.value = '<?= csrf_hash() ?>';
form.appendChild(csrf);
document.body.appendChild(form);
form.submit();
}
);
}
// Handle confirm button click
document.getElementById('confirmModalButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
closeConfirmModal();
}
});
// Close modals on outside click
document.getElementById('resetPasswordModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeResetPasswordModal();
}
});
document.getElementById('confirmModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
</script>
<?= $this->endSection() ?>

331
app/Views/auth/login.php Normal file
View File

@@ -0,0 +1,331 @@
<!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>Sign In - 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') ?>">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body
x-data="{ page: 'comingSoon', 'loaded': true, 'darkMode': false, 'stickyMenu': false, 'sidebarToggle': false, 'scrollTop': false, 'showPassword': false }"
x-init="
darkMode = JSON.parse(localStorage.getItem('darkMode') || 'false');
$watch('darkMode', value => localStorage.setItem('darkMode', JSON.stringify(value)))"
:class="{'dark bg-gray-900': darkMode === true}"
>
<!-- ===== Page Wrapper Start ===== -->
<div class="relative p-6 bg-white z-1 dark:bg-gray-900 sm:p-0">
<div
class="relative flex flex-col justify-center w-full h-screen dark:bg-gray-900 sm:p-0 lg:flex-row"
>
<!-- Form -->
<div class="flex flex-col flex-1 w-full lg:w-1/2">
<div class="w-full max-w-md pt-10 mx-auto">
<a
href="<?= base_url('/') ?>"
class="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<i class="fe fe-arrow-left"></i>
<span class="ml-2">Kembali ke Beranda</span>
</a>
</div>
<div
class="flex flex-col justify-center flex-1 w-full max-w-md mx-auto"
>
<div>
<div class="mb-5 sm:mb-8">
<h1
class="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md"
>
Sign In
</h1>
<p class="text-sm text-gray-500 dark:text-gray-400">
Masukkan username dan password anda!
</p>
</div>
<!-- Flash Messages -->
<?php if (session()->getFlashdata('error')): ?>
<div class="mb-4 p-4 rounded-lg border border-error-200 bg-error-50 dark:border-error-800 dark:bg-error-900/20 shadow-theme-sm">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="flex items-center justify-center w-5 h-5 rounded-full bg-error-100 dark:bg-error-900/40">
<i class="fe fe-alert-circle text-error-600 dark:text-error-400 text-sm"></i>
</div>
</div>
<div class="flex-1">
<p class="text-sm font-semibold text-error-800 dark:text-error-200">
<?= esc(session()->getFlashdata('error')) ?>
</p>
</div>
</div>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="mb-4 p-4 rounded-lg border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-900/20 shadow-theme-sm">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="flex items-center justify-center w-5 h-5 rounded-full bg-green-100 dark:bg-green-900/40">
<i class="fe fe-check-circle text-green-600 dark:text-green-400 text-sm"></i>
</div>
</div>
<div class="flex-1">
<p class="text-sm font-semibold text-green-800 dark:text-green-200">
<?= esc(session()->getFlashdata('success')) ?>
</p>
</div>
</div>
</div>
<?php endif; ?>
<!-- Validation Errors -->
<?php if (isset($validation) && !empty($validation->getErrors())): ?>
<?php foreach ($validation->getErrors() as $field => $error): ?>
<div class="mb-4 p-4 rounded-lg border border-error-200 bg-error-50 dark:border-error-800 dark:bg-error-900/20 shadow-theme-sm">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="flex items-center justify-center w-5 h-5 rounded-full bg-error-100 dark:bg-error-900/40">
<i class="fe fe-alert-circle text-error-600 dark:text-error-400 text-sm"></i>
</div>
</div>
<div class="flex-1">
<p class="text-sm font-semibold text-error-800 dark:text-error-200">
<?= esc($error) ?>
</p>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php
$hasValidationErrors = isset($validation) && !empty($validation->getErrors());
if (isset($error) && !empty($error) && !$hasValidationErrors):
?>
<div class="mb-4 p-4 rounded-lg border border-error-200 bg-error-50 dark:border-error-800 dark:bg-error-900/20 shadow-theme-sm">
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<div class="flex items-center justify-center w-5 h-5 rounded-full bg-error-100 dark:bg-error-900/40">
<i class="fe fe-alert-circle text-error-600 dark:text-error-400 text-sm"></i>
</div>
</div>
<div class="flex-1">
<p class="text-sm font-semibold text-error-800 dark:text-error-200">
<?= esc($error) ?>
</p>
</div>
</div>
</div>
<?php endif; ?>
<!-- Debug: Always show this to test styling -->
<?php if (isset($_GET['debug'])): ?>
<div class="mb-4 p-4 rounded-lg border-2 border-error-500 bg-error-50 dark:border-error-500 dark:bg-error-900/30">
<p class="text-sm font-medium text-error-800 dark:text-error-300">
<i class="fe fe-alert-circle mr-2"></i>
DEBUG: Error message styling test - If you see this, styling works!
</p>
</div>
<?php endif; ?>
<!-- Test: Always show error message for debugging (REMOVE AFTER TESTING) -->
<!--
<div class="mb-4 p-4 rounded-lg border-2 border-error-500 bg-error-50 dark:border-error-500 dark:bg-error-900/30" style="display: block !important; background: #fee2e2 !important; border-color: #ef4444 !important;">
<p class="text-sm font-medium text-error-800 dark:text-error-300" style="color: #991b1b !important;">
<i class="fe fe-alert-circle mr-2"></i>
TEST: Error message test - Jika ini muncul, styling bekerja!
</p>
</div>
-->
<form action="<?= base_url('auth/login') ?>" method="POST" id="loginForm">
<?= csrf_field() ?>
<div class="space-y-5">
<!-- Username -->
<div>
<label
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
>
Username<span class="text-error-500">*</span>
</label>
<input
type="text"
id="username"
name="username"
value="<?= old('username') ?>"
placeholder="Masukkan username"
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
autofocus
/>
<?php if (isset($validation) && $validation->hasError('username')): ?>
<p class="mt-1 text-sm text-error-600"><?= esc($validation->getError('username')) ?></p>
<?php endif; ?>
</div>
<!-- Password -->
<div>
<label
class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
>
Password<span class="text-error-500">*</span>
</label>
<div x-data="{ showPassword: false }" class="relative">
<input
:type="showPassword ? 'text' : 'password'"
id="password"
name="password"
placeholder="Masukkan password"
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent py-2.5 pl-4 pr-11 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
required
/>
<span
@click="showPassword = !showPassword"
class="absolute z-30 text-gray-500 -translate-y-1/2 cursor-pointer right-4 top-1/2 dark:text-gray-400"
>
<i x-show="!showPassword" class="fe fe-eye text-lg"></i>
<i x-show="showPassword" class="fe fe-eye-off text-lg"></i>
</span>
</div>
</div>
<!-- Checkbox -->
<div class="flex items-center justify-between">
<div x-data="{ checkboxToggle: false }">
<label
for="checkboxLabelOne"
class="flex items-center text-sm font-normal text-gray-700 cursor-pointer select-none dark:text-gray-400"
>
<div class="relative">
<input
type="checkbox"
id="checkboxLabelOne"
name="remember"
class="sr-only"
@change="checkboxToggle = !checkboxToggle"
/>
<div
:class="checkboxToggle ? 'border-brand-500 bg-brand-500' : 'bg-transparent border-gray-300 dark:border-gray-700'"
class="mr-3 flex h-5 w-5 items-center justify-center rounded-md border-[1.25px]"
>
<span :class="checkboxToggle ? '' : 'opacity-0'">
<i class="fe fe-check text-white text-xs"></i>
</span>
</div>
</div>
biarkan tetap masuk
</label>
</div>
<a
href="#"
class="text-sm text-brand-500 hover:text-brand-600 dark:text-brand-400"
>lupa password?</a
>
</div>
<!-- Button -->
<div>
<button
type="submit"
class="flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg bg-brand-500 shadow-theme-xs hover:bg-brand-600"
>
Sign In
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div
class="relative items-center hidden w-full h-full bg-brand-950 dark:bg-white/5 lg:grid lg:w-1/2"
>
<div class="flex items-center justify-center z-1">
<!-- ===== Common Grid Shape Start ===== -->
<div class="absolute right-0 top-0 -z-1 w-full max-w-[250px] xl:max-w-[450px]">
<img src="<?= base_url('assets/images/shape/grid-01.svg') ?>" alt="grid" />
</div>
<div
class="absolute bottom-0 left-0 -z-1 w-full max-w-[250px] rotate-180 xl:max-w-[450px]"
>
<img src="<?= base_url('assets/images/shape/grid-01.svg') ?>" alt="grid" />
</div>
<!-- ===== Common Grid Shape End ===== -->
<div class="flex flex-col items-center max-w-xs relative z-10">
<div class="flex items-center gap-3 mb-4">
<a href="<?= base_url('/') ?>" class="block">
<img src="<?= base_url('assets/images/logo/b_logo_1757803697487.png') ?>" alt="Logo Bapenda Garut" class="h-12 w-auto" />
</a>
<h2 class="text-2xl font-bold text-gray-400 dark:text-white">Bapenda Garut</h2>
</div>
</div>
</div>
</div>
<!-- Toggler -->
<div class="fixed z-50 hidden bottom-6 right-6 sm:block">
<button
class="inline-flex items-center justify-center text-white transition-colors rounded-full w-14 h-14 bg-brand-500 hover:bg-brand-600"
@click.prevent="darkMode = !darkMode"
>
<i class="fe fe-sun text-lg hidden dark:block"></i>
<i class="fe fe-moon text-lg dark:hidden"></i>
</button>
</div>
</div>
</div>
<!-- ===== Page Wrapper End ===== -->
<script>
// Debug form submission
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('loginForm');
if (form) {
form.addEventListener('submit', function(e) {
console.log('=== FORM SUBMISSION DEBUG ===');
console.log('Form action:', this.action);
console.log('Form method:', this.method);
const formData = new FormData(this);
const username = formData.get('username');
const password = formData.get('password');
console.log('Username:', username || 'EMPTY!');
console.log('Password:', password ? '***filled***' : 'EMPTY!');
// Check CSRF token
const csrfToken = formData.get('<?= csrf_token() ?>');
console.log('CSRF token:', csrfToken ? 'exists' : 'MISSING!');
// Prevent submission if empty
if (!username || !password) {
console.error('ERROR: Username or password is empty!');
alert('Username dan password harus diisi!');
e.preventDefault();
return false;
}
console.log('Form will be submitted...');
console.log('===========================');
});
} else {
console.error('ERROR: Login form not found!');
}
// Check if error message exists
const errorDiv = document.querySelector('[class*="error-"]');
if (errorDiv) {
console.log('Error message div found:', errorDiv.textContent);
} else {
console.log('No error message div found');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<?php
use CodeIgniter\CLI\CLI;
CLI::error('ERROR: ' . $code);
CLI::write($message);
CLI::newLine();

View File

@@ -0,0 +1,65 @@
<?php
use CodeIgniter\CLI\CLI;
// The main Exception
CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
CLI::write($message);
CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green'));
CLI::newLine();
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
CLI::write(' Caused by:');
CLI::write(' [' . $prevException::class . ']', 'red');
CLI::write(' ' . $prevException->getMessage());
CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
CLI::newLine();
}
// The backtrace
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$backtraces = $last->getTrace();
if ($backtraces) {
CLI::write('Backtrace:', 'green');
}
foreach ($backtraces as $i => $error) {
$padFile = ' '; // 4 spaces
$padClass = ' '; // 7 spaces
$c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
if (isset($error['file'])) {
$filepath = clean_path($error['file']) . ':' . $error['line'];
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
} else {
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
}
$function = '';
if (isset($error['class'])) {
$type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
$function .= $padClass . $error['class'] . $type . $error['function'];
} elseif (! isset($error['class']) && isset($error['function'])) {
$function .= $padClass . $error['function'];
}
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));
$function .= '(' . $args . ')';
CLI::write($function);
CLI::newLine();
}
}

View File

@@ -0,0 +1,5 @@
<?php
// On the CLI, we still want errors in productions
// so just use the exception template.
include __DIR__ . '/error_exception.php';

View File

@@ -0,0 +1,194 @@
:root {
--main-bg-color: #fff;
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #DC4814;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
body {
height: 100%;
background: var(--main-bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--main-text-color);
font-weight: 300;
margin: 0;
padding: 0;
}
h1 {
font-weight: lighter;
font-size: 3rem;
color: var(--dark-text-color);
margin: 0;
}
h1.headline {
margin-top: 20%;
font-size: 5rem;
}
.text-center {
text-align: center;
}
p.lead {
font-size: 1.6rem;
}
.container {
max-width: 75rem;
margin: 0 auto;
padding: 1rem;
}
.header {
background: var(--light-bg-color);
color: var(--dark-text-color);
margin-top: 2.17rem;
}
.header .container {
padding: 1rem;
}
.header h1 {
font-size: 2.5rem;
font-weight: 500;
}
.header p {
font-size: 1.2rem;
margin: 0;
line-height: 2.5;
}
.header a {
color: var(--brand-primary-color);
margin-left: 2rem;
display: none;
text-decoration: none;
}
.header:hover a {
display: inline;
}
.environment {
background: var(--brand-primary-color);
color: var(--main-bg-color);
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
top: 0;
position: fixed;
}
.source {
background: #343434;
color: var(--light-text-color);
padding: 0.5em 1em;
border-radius: 5px;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.85rem;
margin: 0;
overflow-x: scroll;
}
.source span.line {
line-height: 1.4;
}
.source span.line .number {
color: #666;
}
.source .line .highlight {
display: block;
background: var(--dark-text-color);
color: var(--light-text-color);
}
.source span.highlight .number {
color: #fff;
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
padding: 0;
margin-bottom: -1px;
}
.tabs li {
display: inline;
}
.tabs a:link,
.tabs a:visited {
padding: 0 1rem;
line-height: 2.7;
text-decoration: none;
color: var(--dark-text-color);
background: var(--light-bg-color);
border: 1px solid rgba(0,0,0,0.15);
border-bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: inline-block;
}
.tabs a:hover {
background: var(--light-bg-color);
border-color: rgba(0,0,0,0.15);
}
.tabs a.active {
background: var(--main-bg-color);
color: var(--main-text-color);
}
.tab-content {
background: var(--main-bg-color);
border: 1px solid rgba(0,0,0,0.15);
}
.content {
padding: 1rem;
}
.hide {
display: none;
}
.alert {
margin-top: 2rem;
display: block;
text-align: center;
line-height: 3.0;
background: #d9edf7;
border: 1px solid #bcdff1;
border-radius: 5px;
color: #31708f;
}
table {
width: 100%;
overflow: hidden;
}
th {
text-align: left;
border-bottom: 1px solid #e7e7e7;
padding-bottom: 0.5rem;
}
td {
padding: 0.2rem 0.5rem 0.2rem 0;
}
tr:hover td {
background: #f1f1f1;
}
td pre {
white-space: pre-wrap;
}
.trace a {
color: inherit;
}
.trace table {
width: auto;
}
.trace tr td:first-child {
min-width: 5em;
font-weight: bold;
}
.trace td {
background: var(--light-bg-color);
padding: 0 1rem;
}
.trace td pre {
margin: 0;
}
.args {
display: none;
}

View File

@@ -0,0 +1,116 @@
var tabLinks = new Array();
var contentDivs = new Array();
function init()
{
// Grab the tab links and content divs from the page
var tabListItems = document.getElementById('tabs').childNodes;
console.log(tabListItems);
for (var i = 0; i < tabListItems.length; i ++)
{
if (tabListItems[i].nodeName == "LI")
{
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
var id = getHash(tabLink.getAttribute('href'));
tabLinks[id] = tabLink;
contentDivs[id] = document.getElementById(id);
}
}
// Assign onclick events to the tab links, and
// highlight the first tab
var i = 0;
for (var id in tabLinks)
{
tabLinks[id].onclick = showTab;
tabLinks[id].onfocus = function () {
this.blur()
};
if (i == 0)
{
tabLinks[id].className = 'active';
}
i ++;
}
// Hide all content divs except the first
var i = 0;
for (var id in contentDivs)
{
if (i != 0)
{
console.log(contentDivs[id]);
contentDivs[id].className = 'content hide';
}
i ++;
}
}
function showTab()
{
var selectedId = getHash(this.getAttribute('href'));
// Highlight the selected tab, and dim all others.
// Also show the selected content div, and hide all others.
for (var id in contentDivs)
{
if (id == selectedId)
{
tabLinks[id].className = 'active';
contentDivs[id].className = 'content';
}
else
{
tabLinks[id].className = '';
contentDivs[id].className = 'content hide';
}
}
// Stop the browser following the link
return false;
}
function getFirstChildWithTagName(element, tagName)
{
for (var i = 0; i < element.childNodes.length; i ++)
{
if (element.childNodes[i].nodeName == tagName)
{
return element.childNodes[i];
}
}
}
function getHash(url)
{
var hashPos = url.lastIndexOf('#');
return url.substring(hashPos + 1);
}
function toggle(elem)
{
elem = document.getElementById(elem);
if (elem.style && elem.style['display'])
{
// Only works with the "style" attr
var disp = elem.style['display'];
}
else if (elem.currentStyle)
{
// For MSIE, naturally
var disp = elem.currentStyle['display'];
}
else if (window.getComputedStyle)
{
// For most other browsers
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
}
// Toggle the state of the "display" style
elem.style.display = disp == 'block' ? 'none' : 'block';
return false;
}

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.badRequest') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.pageNotFound') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>404</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryCannotFind') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,429 @@
<?php
use CodeIgniter\HTTP\Header;
use CodeIgniter\CodeIgniter;
$errorId = uniqid('error', true);
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<script>
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:s')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
</div>
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
</div>
</div>
<!-- Source -->
<div class="container">
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
<div class="container">
<?php
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php
}
?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<tbody>
<tr>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<?php $empty = false; ?>
<h3>$<?= esc($var) ?></h3>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<!-- Memory -->
<div class="content" id="memory">
<table>
<tbody>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /tab-content -->
</div> <!-- /container -->
<?php endif; ?>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= lang('Errors.whoops') ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
</head>
<body>
<div class="container text-center">
<h1 class="headline"><?= lang('Errors.whoops') ?></h1>
<p class="lead"><?= lang('Errors.weHitASnag') ?></p>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long