%PDF- <> %âãÏÓ endobj 2 0 obj <> endobj 3 0 obj <>/ExtGState<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI] >>/Annots[ 28 0 R 29 0 R] /MediaBox[ 0 0 595.5 842.25] /Contents 4 0 R/Group<>/Tabs/S>> endobj ºaâÚÎΞ-ÌE1ÍØÄ÷{òò2ÿ ÛÖ^ÔÀá TÎ{¦?§®¥kuµùÕ5sLOšuY>endobj 2 0 obj<>endobj 2 0 obj<>endobj 2 0 obj<>endobj 2 0 obj<> endobj 2 0 obj<>endobj 2 0 obj<>es 3 0 R>> endobj 2 0 obj<> ox[ 0.000000 0.000000 609.600000 935.600000]/Fi endobj 3 0 obj<> endobj 7 1 obj<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>>/Subtype/Form>> stream
<?php
/**
* Kumpulan helper function bersama.
* PPID Kabupaten Mamuju Tengah
*/
/**
* Escape output HTML untuk mencegah XSS.
*/
function e(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* Ekstrak ID video YouTube (11 karakter) dari berbagai format URL/ID.
* Menerima: watch?v=, youtu.be/, embed/, shorts/, atau ID mentah.
* Kembalikan '' bila tidak valid.
*/
function youtube_id_from_url(string $input): string
{
$input = trim($input);
if ($input === '') {
return '';
}
// Sudah berupa ID mentah (11 char valid).
if (preg_match('~^[A-Za-z0-9_-]{11}$~', $input)) {
return $input;
}
if (preg_match('~(?:youtube\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|live/)|youtu\.be/)([A-Za-z0-9_-]{11})~', $input, $m)) {
return $m[1];
}
return '';
}
/**
* Bersihkan HTML dari editor (WYSIWYG) — hanya izinkan tag aman, buang event handler
* & URI javascript:. Dipakai untuk konten kaya (mis. isi berita).
*/
function sanitize_html(string $html): string
{
$allowed = '<p><br><b><strong><i><em><u><s><ul><ol><li><h2><h3><h4><blockquote><a><span><div>';
$html = strip_tags($html, $allowed);
// Buang atribut event handler (onclick, onerror, dst.).
$html = preg_replace('/\son\w+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $html);
// Netralkan URI javascript: pada href/src.
$html = preg_replace('/(href|src)\s*=\s*(["\'])\s*javascript:[^"\']*\2/i', '$1="#"', $html);
return trim((string) $html);
}
/**
* Membuat URL absolut berdasarkan BASE_URL — TANPA ekstensi .php (URL bersih).
*
* Ekstensi .php pada nama file dibuang agar tautan tampil bersih
* (mis. url('profil.php') → /profil, url('berita-detail.php?slug=x') → /berita-detail?slug=x).
* Segmen 'index' dipetakan ke root direktorinya (index.php → /, admin/index.php → /admin/).
* Query string (?) dan fragment (#) tetap dipertahankan.
* Aturan rewrite di .htaccess menerjemahkan URL bersih kembali ke file .php.
*/
function url(string $path = ''): string
{
$path = ltrim($path, '/');
// Pisahkan bagian nama file dari query/fragment agar '.php' hanya dibuang dari nama file.
$cut = strcspn($path, '?#');
$file = substr($path, 0, $cut);
$rest = substr($path, $cut); // '' atau diawali '?' / '#'
// Buang ekstensi .php, lalu petakan segmen 'index' ke root direktorinya.
$file = preg_replace('~\.php$~i', '', $file);
$file = preg_replace('~(^|/)index$~i', '$1', $file);
return BASE_URL . '/' . ltrim($file . $rest, '/');
}
/**
* URL aset (gambar, css, js).
*/
function asset(string $path = ''): string
{
return BASE_URL . '/assets/' . ltrim($path, '/');
}
/**
* URL aset dengan penanda versi (?v=mtime) untuk cache-busting.
* Dipakai untuk file yang di-build ulang tetapi namanya tetap (mis. css/app.css),
* agar browser mengambil versi baru meski cache-nya panjang.
*/
function asset_ver(string $path): string
{
$full = ROOT_PATH . '/assets/' . ltrim($path, '/');
$v = is_file($full) ? (string) filemtime($full) : '1';
return asset($path) . '?v=' . $v;
}
/**
* Redirect ke URL relatif (terhadap BASE_URL) lalu hentikan eksekusi.
*/
function redirect(string $path): void
{
header('Location: ' . url($path));
exit;
}
/**
* Redirect ke URL absolut apa adanya.
*/
function redirect_to(string $absoluteUrl): void
{
header('Location: ' . $absoluteUrl);
exit;
}
/**
* Set flash message yang tampil sekali pada request berikutnya.
*
* @param string $type success|error|info|warning
*/
function set_flash(string $type, string $message): void
{
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
}
/**
* Ambil & hapus semua flash message.
*
* @return array<int, array{type:string, message:string}>
*/
function get_flashes(): array
{
$flashes = $_SESSION['flash'] ?? [];
unset($_SESSION['flash']);
return $flashes;
}
/**
* Ambil nilai dari $_GET dengan default & trim.
*/
function get_param(string $key, string $default = ''): string
{
return isset($_GET[$key]) ? trim((string) $_GET[$key]) : $default;
}
/**
* Ambil nilai dari $_POST dengan default & trim.
*/
function post_param(string $key, string $default = ''): string
{
return isset($_POST[$key]) ? trim((string) $_POST[$key]) : $default;
}
/**
* Format tanggal Indonesia, contoh: "26 Jun 2026".
*/
function tanggal_id(?string $datetime, bool $withTime = false): string
{
if (empty($datetime)) {
return '-';
}
$bulan = [
1 => 'Jan', 2 => 'Feb', 3 => 'Mar', 4 => 'Apr', 5 => 'Mei', 6 => 'Jun',
7 => 'Jul', 8 => 'Agu', 9 => 'Sep', 10 => 'Okt', 11 => 'Nov', 12 => 'Des',
];
$ts = strtotime($datetime);
if ($ts === false) {
return '-';
}
$out = (int) date('j', $ts) . ' ' . $bulan[(int) date('n', $ts)] . ' ' . date('Y', $ts);
if ($withTime) {
$out .= ', ' . date('H:i', $ts) . ' WITA';
}
return $out;
}
/**
* Format ukuran file menjadi human-readable.
*/
function format_size(int $bytes): string
{
if ($bytes <= 0) {
return '0 KB';
}
$units = ['B', 'KB', 'MB', 'GB'];
$i = (int) floor(log($bytes, 1024));
$i = min($i, count($units) - 1);
return round($bytes / (1024 ** $i), 1) . ' ' . $units[$i];
}
function format_durasi(int $detik): string
{
$detik = max(0, $detik);
$j = intdiv($detik, 3600);
$m = intdiv($detik % 3600, 60);
$s = $detik % 60;
return $j > 0 ? sprintf('%d:%02d:%02d', $j, $m, $s) : sprintf('%d:%02d', $m, $s);
}
function analytics_total_visitors(): int
{
try {
return (int) db()->query('SELECT COUNT(*) FROM analytics_visitor')->fetchColumn();
} catch (Throwable $e) {
return 0;
}
}
/**
* Membuat nomor resi permohonan unik: REQ-YYMMDD-XXX.
*/
function generate_resi(): string
{
return 'REQ-' . date('ymd') . '-' . strtoupper(substr(bin2hex(random_bytes(3)), 0, 5));
}
/**
* Membuat nomor resi keberatan: KBR-YYMMDD-XXX.
*/
function generate_resi_keberatan(): string
{
return 'KBR-' . date('ymd') . '-' . strtoupper(substr(bin2hex(random_bytes(3)), 0, 5));
}
/**
* Definisi status permohonan: label, warna badge (tailwind), ikon.
*
* @return array<string, array{label:string, badge:string, icon:string}>
*/
function status_permohonan_meta(): array
{
return [
'verifikasi' => [
'label' => 'Verifikasi',
'badge' => 'bg-yellow-50 text-mateng-gold border-yellow-200',
'icon' => 'fa-hourglass-half',
],
'diproses' => [
'label' => 'Diproses',
'badge' => 'bg-blue-50 text-mateng-blue border-blue-200',
'icon' => 'fa-spinner',
],
'selesai' => [
'label' => 'Selesai',
'badge' => 'bg-green-50 text-mateng-green border-green-200',
'icon' => 'fa-check-circle',
],
'ditolak' => [
'label' => 'Ditolak',
'badge' => 'bg-red-50 text-red-600 border-red-200',
'icon' => 'fa-times-circle',
],
];
}
/**
* Render badge HTML untuk status permohonan.
*/
function badge_status(string $status): string
{
$meta = status_permohonan_meta();
$info = $meta[$status] ?? [
'label' => ucfirst($status),
'badge' => 'bg-slate-100 text-slate-600 border-slate-200',
'icon' => 'fa-circle',
];
return sprintf(
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold border %s">'
. '<i class="fas %s text-[10px]"></i> %s</span>',
$info['badge'],
$info['icon'],
e($info['label'])
);
}
/**
* Render deretan bintang rating (1..5) sebagai ikon FontAwesome.
*
* @param int|float $rating Nilai 0..5.
* @param string $size Kelas ukuran Tailwind, mis. 'text-base'.
*/
function render_stars($rating, string $size = 'text-base'): string
{
$rating = (int) round((float) $rating);
$rating = max(0, min(5, $rating));
$out = '<span class="inline-flex items-center gap-0.5 text-mateng-gold ' . $size . '">';
for ($i = 1; $i <= 5; $i++) {
$out .= '<i class="' . ($i <= $rating ? 'fas' : 'far') . ' fa-star"></i>';
}
$out .= '</span>';
return $out;
}
/**
* Potong teks dengan ellipsis.
*/
function excerpt(?string $text, int $length = 120): string
{
$text = trim(strip_tags((string) $text));
// Gunakan fungsi multibyte bila tersedia, jika tidak jatuh ke fungsi standar.
if (function_exists('mb_strlen')) {
if (mb_strlen($text) <= $length) {
return $text;
}
return mb_substr($text, 0, $length) . '…';
}
if (strlen($text) <= $length) {
return $text;
}
return substr($text, 0, $length) . '…';
}
/**
* Membuat slug URL-friendly dari sebuah teks.
*/
function slugify(string $text): string
{
$text = strtolower(trim($text));
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
return trim($text, '-') ?: 'item';
}
/**
* Validasi format email sederhana.
*/
function is_valid_email(string $email): bool
{
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Memetakan nama warna kategori ke kelas Tailwind (untuk ikon & teks).
*
* @return array{icon_bg:string, icon_text:string, link:string}
*/
function kategori_warna(string $warna): array
{
return match ($warna) {
'blue' => ['icon_bg' => 'bg-blue-50', 'icon_text' => 'text-mateng-blue', 'link' => 'text-mateng-blue hover:text-sky-700'],
'red' => ['icon_bg' => 'bg-red-50', 'icon_text' => 'text-red-600', 'link' => 'text-red-600 hover:text-red-700'],
'green' => ['icon_bg' => 'bg-green-50', 'icon_text' => 'text-mateng-green', 'link' => 'text-mateng-green hover:text-green-800'],
'gold' => ['icon_bg' => 'bg-yellow-50','icon_text' => 'text-mateng-gold', 'link' => 'text-mateng-gold hover:text-yellow-700'],
'slate' => ['icon_bg' => 'bg-slate-100','icon_text' => 'text-slate-600', 'link' => 'text-slate-600 hover:text-slate-800'],
default => ['icon_bg' => 'bg-slate-100','icon_text' => 'text-slate-600', 'link' => 'text-slate-600 hover:text-slate-800'],
};
}
/**
* Menentukan ikon FontAwesome berdasarkan ekstensi file.
*/
function file_icon(string $filename): string
{
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
return match ($ext) {
'pdf' => 'fa-file-pdf text-red-500',
'doc', 'docx' => 'fa-file-word text-blue-500',
'xls', 'xlsx' => 'fa-file-excel text-green-600',
'ppt', 'pptx' => 'fa-file-powerpoint text-orange-500',
'jpg', 'jpeg', 'png', 'gif' => 'fa-file-image text-purple-500',
'zip', 'rar' => 'fa-file-archive text-amber-600',
default => 'fa-file text-slate-400',
};
}