Merge branch 'feature/bilingual-support'

This commit is contained in:
cygouw
2026-06-05 08:33:51 +07:00
11 changed files with 2796 additions and 41 deletions
+80
View File
@@ -569,6 +569,46 @@ const translations = {
kontrib_guideline_3: "Sertakan fakta dan data yang akurat dan dapat diverifikasi.",
kontrib_guideline_4: "Hindari plagiarisme. Karya harus orisinal.",
kontrib_guideline_5: "Tim redaksi berhak menyunting konten untuk keperluan editorial.",
// --- Group C4A: Staf Pendukung & SOP ---
grp_c4a_hero_tag: "LAYANAN AKADEMIK \u2022 INFORMATIKA UNTAN",
grp_c4a_hero_title: "Staf Pendukung &amp; <br/><span class=\"highlight-orange\">Standar Operasional Prosedur (SOP)</span>",
grp_c4a_hero_desc: "Halaman ini menampilkan informasi petugas layanan akademik serta prosedur administrasi mahasiswa Program Studi Informatika UNTAN.",
grp_c4a_stat_staf: "Staf Aktif",
grp_c4a_stat_sop: "Jenis SOP",
grp_c4a_staff_tag: "Tim Kami",
grp_c4a_staff_title: "Staf Pendukung",
grp_c4a_staff_subtitle: "Staf yang siap melayani kebutuhan akademik dan operasional laboratorium di Prodi Informatika UNTAN.",
grp_c4a_sop_tag: "Prosedur Resmi",
grp_c4a_sop_title: "Standar Operasional Prosedur (SOP)",
grp_c4a_sop_subtitle: "Standar Operasional Prosedur untuk berbagai kebutuhan administrasi akademik mahasiswa. Gunakan pencarian atau filter untuk menemukan SOP yang Anda butuhkan.",
grp_c4a_sop_search_placeholder: "Cari SOP berdasarkan judul...",
grp_c4a_sop_chip_semua: "Semua",
grp_c4a_sop_chip_akademik: "Akademik",
grp_c4a_sop_chip_praktikum: "Praktikum",
grp_c4a_sop_empty_title: "SOP tidak ditemukan",
grp_c4a_sop_empty_desc: "Coba kata kunci lain atau reset filter pencarian.",
grp_c4a_sop_reset_btn: "Reset Pencarian",
grp_c4a_faq_tag: "Pertanyaan Umum",
grp_c4a_faq_title: "Pertanyaan yang Sering Diajukan",
grp_c4a_faq_subtitle: "Temukan jawaban cepat atas pertanyaan umum mahasiswa seputar layanan akademik prodi.",
grp_c4a_empty_staff: "Data staf pendukung tidak tersedia saat ini.",
grp_c4a_empty_sop: "Data SOP tidak tersedia saat ini.",
grp_c4a_empty_faq: "Data FAQ tidak tersedia saat ini.",
grp_c4a_label_location: "Lokasi",
grp_c4a_label_service_hours: "Jam Layanan",
grp_c4a_label_wa_contact: "Kontak WhatsApp",
grp_c4a_label_tasks: "Tugas & Tanggung Jawab",
grp_c4a_label_send_wa: "Kirim Pesan WA",
grp_c4a_label_steps: "Langkah",
grp_c4a_label_view_detail: "Lihat Detail",
grp_c4a_label_responsible_staff: "Staf Bertanggung Jawab (Bersifat Opsional)",
grp_c4a_label_additional_info: "Informasi Tambahan (Bersifat Opsional):",
grp_c4a_label_purpose: "Tujuan",
grp_c4a_label_requirements: "Persyaratan",
grp_c4a_label_procedure: "Alur Prosedur",
grp_c4a_sop_showing_all: "Menampilkan {total} SOP",
grp_c4a_sop_showing_filtered: "Menampilkan {shown} dari {total} SOP",
},
en: {
@@ -1135,6 +1175,46 @@ const translations = {
kontrib_guideline_3: "Include accurate and verifiable facts and data.",
kontrib_guideline_4: "Avoid plagiarism. Work must be original.",
kontrib_guideline_5: "The editorial team reserves the right to edit content for editorial purposes.",
// --- Group C4A: Supporting Staff & SOP ---
grp_c4a_hero_tag: "ACADEMIC SERVICES \u2022 INFORMATICS UNTAN",
grp_c4a_hero_title: "Supporting Staff &amp; <br/><span class=\"highlight-orange\">Standard Operating Procedures (SOP)</span>",
grp_c4a_hero_desc: "This page presents information about academic service officers and administrative procedures for students of the Informatics Study Program at UNTAN.",
grp_c4a_stat_staf: "Active Staff",
grp_c4a_stat_sop: "SOP Types",
grp_c4a_staff_tag: "Our Team",
grp_c4a_staff_title: "Supporting Staff",
grp_c4a_staff_subtitle: "Staff ready to serve academic and laboratory operational needs at the Informatics Study Program, UNTAN.",
grp_c4a_sop_tag: "Official Procedures",
grp_c4a_sop_title: "Standard Operating Procedures (SOP)",
grp_c4a_sop_subtitle: "Standard Operating Procedures for various student academic administration needs. Use search or filter to find the SOP you need.",
grp_c4a_sop_search_placeholder: "Search SOP by title...",
grp_c4a_sop_chip_semua: "All",
grp_c4a_sop_chip_akademik: "Academic",
grp_c4a_sop_chip_praktikum: "Practicum",
grp_c4a_sop_empty_title: "No SOP found",
grp_c4a_sop_empty_desc: "Try another keyword or reset the search filter.",
grp_c4a_sop_reset_btn: "Reset Search",
grp_c4a_faq_tag: "Frequently Asked",
grp_c4a_faq_title: "Frequently Asked Questions",
grp_c4a_faq_subtitle: "Find quick answers to common student questions about the study program's academic services.",
grp_c4a_empty_staff: "Staff data is currently unavailable.",
grp_c4a_empty_sop: "SOP data is currently unavailable.",
grp_c4a_empty_faq: "FAQ data is currently unavailable.",
grp_c4a_label_location: "Location",
grp_c4a_label_service_hours: "Service Hours",
grp_c4a_label_wa_contact: "WhatsApp Contact",
grp_c4a_label_tasks: "Tasks & Responsibilities",
grp_c4a_label_send_wa: "Send WA Message",
grp_c4a_label_steps: "Steps",
grp_c4a_label_view_detail: "View Details",
grp_c4a_label_responsible_staff: "Responsible Staff (Optional)",
grp_c4a_label_additional_info: "Additional Information (Optional):",
grp_c4a_label_purpose: "Purpose",
grp_c4a_label_requirements: "Requirements",
grp_c4a_label_procedure: "Procedure Flow",
grp_c4a_sop_showing_all: "Showing {total} SOP",
grp_c4a_sop_showing_filtered: "Showing {shown} of {total} SOP",
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

+52
View File
@@ -0,0 +1,52 @@
[
{
"id": 1,
"question": "Apa saja syarat administratif agar saya bisa mengikuti perkuliahan?",
"answer": "Syarat untuk mengikuti perkuliahan adalah mahasiswa telah melakukan <strong>pembayaran UKT</strong>, mengisi <strong>KRS</strong>, dan terdaftar di <strong>kelas Edlink</strong>."
},
{
"id": 2,
"question": "Berapa persentase kehadiran minimal agar bisa mengikuti Ujian Akhir Semester (UAS)?",
"answer": "Mahasiswa wajib memenuhi minimal <strong>75% kehadiran</strong> dari total pertemuan perkuliahan. Mahasiswa yang tidak memenuhi syarat kehadiran ini tidak diperkenankan mengikuti UAS."
},
{
"id": 3,
"question": "Apakah pengisian evaluasi perkuliahan itu wajib?",
"answer": "Ya, karena pengisian evaluasi perkuliahan dapat menjadi salah satu syarat untuk <strong>mengakses hasil studi</strong> sesuai dengan kebijakan yang berlaku. Hasil ini sangat penting untuk perbaikan dan pengembangan kualitas pembelajaran prodi ke depannya."
},
{
"id": 4,
"question": "Berapa minimal SKS dan IPK untuk bisa mendaftar Kerja Praktik (KP)?",
"answer": "Untuk mendaftar KP, mahasiswa harus sudah lulus minimal <strong>100 SKS</strong> dan memiliki Indeks Prestasi Kumulatif (IPK) <strong>≥ 2,00</strong>."
},
{
"id": 5,
"question": "Apakah ada syarat kelulusan mata kuliah tertentu sebelum saya bisa mengajukan Kerja Praktik (KP)?",
"answer": "Ya, selain memenuhi syarat minimal SKS dan IPK, mahasiswa juga diwajibkan telah lulus mata kuliah <strong>Penulisan Proposal Tugas Akhir dengan nilai minimal C</strong>. Selain itu, mahasiswa juga harus menyerahkan berkas pendukung pengajuan KP seperti KTM, riwayat studi, transkrip nilai, dan surat permohonan."
},
{
"id": 6,
"question": "Apa kualifikasi yang dibutuhkan untuk Pembimbing Lapangan di instansi tempat KP?",
"answer": "Pembimbing Lapangan dari tempat pelaksanaan KP diwajibkan memiliki kualifikasi pendidikan minimal <strong>S1 atau D4</strong>."
},
{
"id": 7,
"question": "Bolehkah saya mengambil mata kuliah Kerja Praktik (KP) bersamaan dengan Tugas Akhir?",
"answer": "Boleh. Mata Kuliah Kerja Praktik (KP) dapat diambil secara bersamaan dengan <strong>Tugas Akhir 1 (TA1)</strong> dan <strong>Tugas Akhir 2 (TA2)</strong> dalam satu semester yang sama."
},
{
"id": 8,
"question": "Apa saja syarat akademik utama sebelum mengajukan judul Tugas Akhir (Skripsi)?",
"answer": "Syarat utamanya adalah mahasiswa harus telah lulus minimal <strong>120 SKS</strong>, memiliki <strong>IPK minimal 2,00</strong>, telah lulus mata kuliah <strong>Penulisan Proposal Tugas Akhir dengan nilai minimal C</strong>, serta telah mendaftarkan mata kuliah skripsi pada KRS."
},
{
"id": 9,
"question": "Siapa yang berhak mendaftar menjadi Asisten Praktikum (Aslab)?",
"answer": "Rekrutmen terbuka bagi <strong>mahasiswa aktif Program Studi Informatika UNTAN</strong> yang telah lulus mata kuliah terkait (dengan nilai minimal sesuai ketentuan Dosen Pengampu) dan berkomitmen mengikuti seluruh jadwal praktikum selama semester berjalan."
},
{
"id": 10,
"question": "Bagaimana prosedur meminjam Laboratorium Komputer untuk keperluan kegiatan kampus?",
"answer": "Peminjam (dosen atau mahasiswa) harus mengajukan <strong>surat permohonan peminjaman</strong> kepada staf laboratorium pengelola dan menyampaikan jadwal penggunaannya. Penggunaan lab juga harus didampingi oleh <strong>dosen atau asisten praktikum</strong> yang bertanggung jawab."
}
]
+197
View File
@@ -0,0 +1,197 @@
[
{
"tab_id": "kuliah",
"tab_number": 1,
"category": "akademik",
"icon": "fa-chalkboard-teacher",
"title": "SOP Pelaksanaan Kuliah",
"description": "Mengatur tata cara dan tahapan pelaksanaan perkuliahan mulai dari persiapan hingga evaluasi pembelajaran.",
"purpose": "SOP ini bertujuan untuk menjelaskan tata cara pelaksanaan perkuliahan bagi mahasiswa Program Studi Informatika Universitas Tanjungpura.",
"requirements": [
"Mahasiswa telah melakukan pembayaran UKT",
"Mahasiswa telah mengisi KRS",
"Mahasiswa terdaftar di kelas Edlink",
"Mahasiswa wajib mengikuti perkuliahan dan menyelesaikan tugas tepat waktu",
"Mahasiswa wajib mengisi daftar hadir dan memberikan alasan resmi jika tidak hadir",
"Mahasiswa memenuhi minimal 70% kehadiran untuk mengikuti UAS"
],
"procedures": [
"Dosen memasuki ruang perkuliahan sesuai jadwal yang telah ditetapkan",
"Dosen mengabsensi mahasiswa dengan mencantumkan keterangan hadir, izin, atau tidak hadir",
"Pada pertemuan pertama, dosen membuat kontrak perkuliahan secara tertulis bersama mahasiswa",
"Dosen menjelaskan materi perkuliahan sesuai RPS yang telah diserahkan ke program studi",
"Mahasiswa mendiskusikan, mencatat, dan mengkritisi materi perkuliahan",
"Dosen menyimpulkan materi perkuliahan dan memberikan penugasan"
],
"additional_info": "Mahasiswa yang tidak memenuhi persyaratan kehadiran minimal tidak diperkenankan mengikuti Ujian Akhir Semester (UAS) sesuai ketentuan akademik yang berlaku."
},
{
"tab_id": "ujian",
"tab_number": 2,
"category": "akademik",
"icon": "fa-file-signature",
"title": "SOP Pelaksanaan Ujian (UTS & UAS)",
"description": "Mengatur pedoman pelaksanaan Ujian Tengah Semester (UTS) dan Ujian Akhir Semester (UAS) mulai dari penentuan kriteria evaluasi, pelaksanaan, hingga perekapan nilai.",
"purpose": "SOP ini bertujuan untuk memastikan kelancaran proses evaluasi pembelajaran mahasiswa melalui UTS dan UAS agar berjalan tertib, terukur, dan sesuai dengan instruksi dosen pengampu mata kuliah.",
"requirements": [
"Mahasiswa terdaftar aktif pada mata kuliah yang sedang berjalan pada semester tersebut",
"Persentase kehadiran perkuliahan mahasiswa tidak kurang dari 75%"
],
"procedures": [
"Dosen pengampu menentukan kriteria UTS dan UAS",
"Mahasiswa mengikuti UTS atau UAS sesuai instruksi dosen pengampu, dan wajib melakukan presensi",
"Mahasiswa mengumpulkan hasil UTS atau UAS sesuai rentang waktu yang diberikan",
"Dosen menilai dan merekap pengerjaan UTS atau UAS mahasiswa"
],
"additional_info": "Segala bentuk kecurangan saat ujian, maupun tindakan plagiarisme dalam pengerjaan UTS/UAS, dapat mengakibatkan pembatalan nilai atau pemberian sanksi."
},
{
"tab_id": "evaluasi",
"tab_number": 3,
"category": "akademik",
"icon": "fa-chart-line",
"title": "SOP Pelaksanaan Evaluasi Pengembangan Perkuliahan",
"description": "Mengatur tata cara pelaksanaan evaluasi perkuliahan oleh mahasiswa sebagai sarana umpan balik untuk meningkatkan kualitas proses belajar mengajar.",
"purpose": "SOP ini bertujuan memberikan pedoman pelaksanaan evaluasi perkuliahan guna mendukung peningkatan mutu pembelajaran di Program Studi Informatika Universitas Tanjungpura.",
"requirements": [
"Mahasiswa terdaftar aktif pada semester berjalan",
"Telah mengikuti kegiatan perkuliahan pada mata kuliah yang dievaluasi",
"Tersedia akses ke sistem evaluasi perkuliahan"
],
"procedures": [
"Program Studi membuka periode evaluasi perkuliahan",
"Mahasiswa mengakses satu untan dan mengisi kuesioner evaluasi perkuliahan",
"Sistem merekap hasil evaluasi yang telah diisi mahasiswa",
"Program Studi meninjau hasil evaluasi sebagai bahan pengembangan perkuliahan"
],
"additional_info": "Hasil evaluasi digunakan sebagai dasar perbaikan dan pengembangan kualitas pembelajaran pada semester berikutnya. Pengisian evaluasi perkuliahan dapat menjadi salah satu syarat untuk mengakses hasil studi sesuai kebijakan yang berlaku."
},
{
"tab_id": "kp",
"tab_number": 4,
"category": "akademik",
"icon": "fa-briefcase",
"title": "SOP Kerja Praktik",
"description": "Panduan resmi pelaksanaan Kerja Praktek (KP) mulai dari tata cara pendaftaran, pelaksanaan, hingga seminar Kerja Praktek (KP).",
"purpose": "SOP ini bertujuan memberikan pedoman kepada seluruh pihak terkait dalam pelaksanaan Kerja Praktek serta meningkatkan efisiensi dan efektivitas pelaksanaannya di Program Studi Informatika Fakultas Teknik Universitas Tanjungpura.",
"requirements": [
"Telah lulus minimal 100 SKS",
"Memiliki IPK minimal 2,00",
"Telah lulus mata kuliah Penulisan Proposal Tugas Akhir dengan nilai minimal C",
"Menyerahkan berkas pendukung pengajuan KP (KTM, riwayat studi, transkrip, dan surat permohonan)",
"Pembimbing Lapangan dari tempat KP memiliki kualifikasi pendidikan minimal S1/D4"
],
"procedures": [
"Mahasiswa mengusulkan topik, draf proposal, dan menyerahkan berkas KP",
"Staf Administrasi memverifikasi kelengkapan berkas",
"Ketua Prodi menerbitkan surat pengantar KP",
"Mahasiswa melaksanakan KP dan mengikuti bimbingan",
"Pembimbing Lapangan memberikan penilaian KP",
"Mahasiswa mengajukan seminar dan Prodi menerbitkan undangan",
"Mahasiswa melaksanakan seminar KP",
"Mahasiswa melakukan revisi laporan akhir KP"
],
"staff_responsible": [
"👤 Staf Administrasi Informatika"
],
"additional_info": "Mata Kuliah Kerja Praktik (KP) dapat diambil bersamaan dengan Tugas Akhir 1 (TA1) dan Tugas Akhir 2 (TA2) dalam semester yang sama."
},
{
"tab_id": "ta",
"tab_number": 5,
"category": "akademik",
"icon": "fa-graduation-cap",
"title": "SOP Tugas Akhir/Skripsi",
"description": "Mengatur tata cara pelaksanaan skripsi mulai dari pengajuan judul hingga ujian dan penyelesaian skripsi mahasiswa.",
"purpose": "SOP ini bertujuan untuk memberikan pedoman kepada mahasiswa dan dosen dalam pelaksanaan skripsi agar proses penyusunan skripsi berjalan tertib, terarah, dan sesuai ketentuan akademik.",
"requirements": [
"Telah lulus minimal 120 SKS",
"Memiliki IPK minimal 2,00",
"Telah lulus mata kuliah Penulisan Proposal Tugas Akhir dengan nilai minimal C",
"Telah mendaftarkan mata kuliah skripsi pada KRS",
"Telah menyiapkan judul dan proposal skripsi",
"Memiliki dosen pembimbing skripsi",
"Melengkapi berkas persyaratan"
],
"procedures": [
"Mahasiswa mengajukan judul dan proposal skripsi",
"Program studi menetapkan dosen pembimbing skripsi",
"Mahasiswa melakukan bimbingan dan penyusunan skripsi",
"Mahasiswa mengajukan seminar atau sidang skripsi",
"Tim penguji dan pembimbing melaksanakan seminar skripsi",
"Mahasiswa melakukan revisi skripsi",
"Mahasiswa menyerahkan skripsi final"
],
"staff_responsible": [
"👤 Staf Administrasi Informatika"
],
"additional_info": "Mahasiswa diberi waktu maksimal 2 semester untuk menyelesaikan Tugas Akhir. Perpanjangan dapat dilakukan dengan persetujuan tertulis dari Kaprodi dan Pembimbing."
},
{
"tab_id": "praktikum",
"tab_number": 6,
"category": "praktikum",
"icon": "fa-flask",
"title": "SOP Praktikum Mata Kuliah",
"description": "Mengatur tata cara pelaksanaan praktikum mata kuliah agar kegiatan praktikum berjalan tertib, aman, dan sesuai ketentuan pembelajaran.",
"purpose": "SOP ini bertujuan untuk menjelaskan tata cara pelaksanaan perkuliahan yang memiliki komponen praktikum pada semester berjalan.",
"requirements": [
"Terdaftar sebagai peserta mata kuliah yang memiliki komponen praktikum pada semester berjalan",
"Mahasiswa membawa perlengkapan praktikum yang diperlukan"
],
"procedures": [
"Dosen mata kuliah menuntaskan pembahasan materi yang menjadi dasar praktikum",
"Mahasiswa mengisi daftar hadir dan menerima penjelasan prosedur praktikum",
"Mahasiswa melaksanakan praktikum sesuai modul di bawah pengawasan dosen atau asisten praktikum",
"Mahasiswa menyusun dan mengumpulkan laporan praktikum sesuai tenggat waktu",
"Dosen atau asisten melakukan penilaian laporan dan hasil praktikum sebagai komponen nilai akhir"
],
"additional_info": "Mahasiswa wajib mengikuti seluruh ketentuan praktikum dan menjaga ketertiban serta keamanan selama kegiatan praktikum berlangsung."
},
{
"tab_id": "rekrut_asprak",
"tab_number": 7,
"category": "praktikum",
"icon": "fa-user-plus",
"title": "SOP Perekrutan Asisten Praktikum",
"description": "Prosedur perekrutan Asisten Praktikum (Aslab) mulai dari pendataan kebutuhan mata kuliah, proses seleksi, hingga pengumuman kelulusan.",
"purpose": "SOP ini bertujuan sebagai panduan baku untuk menyelenggarakan rekrutmen Asisten Praktikum secara transparan, guna menyaring mahasiswa Informatika UNTAN yang kompeten dan berkomitmen sesuai kualifikasi mata kuliah.",
"requirements": [
"Mahasiswa aktif Program Studi Informatika UNTAN",
"Lulus mata kuliah terkait dengan nilai minimal ketentuan Dosen Pengampu",
"Berkomitmen mengikuti seluruh jadwal dan kontrak praktikum selama semester berjalan"
],
"procedures": [
"Dosen mendata kebutuhan kuota dan kualifikasi Asisten Praktikum",
"Dosen mengumumkan pembukaan rekrutmen asisten praktikum",
"Mahasiswa mendaftarkan diri sesuai persyaratan yang diberikan",
"Dosen melakukan seleksi wawancara kepada calon asisten praktikum",
"Dosen mengumumkan calon yang diterima berdasarkan hasil seleksi"
],
"additional_info": "Keputusan hasil seleksi bersifat final dan ditetapkan oleh dosen pengampu mata kuliah. Asisten Praktikum yang terpilih wajib mematuhi seluruh ketentuan yang berlaku selama menjalankan tugas sebagai Asisten Praktikum."
},
{
"tab_id": "labkom",
"tab_number": 8,
"category": "praktikum",
"icon": "fa-desktop",
"title": "SOP Peminjaman Laboratorium Komputer",
"description": "Mengatur tata cara peminjaman dan penggunaan laboratorium komputer untuk mendukung kegiatan praktikum dan pembelajaran.",
"purpose": "SOP ini bertujuan menjelaskan tata cara peminjaman lab komputer dan memastikan peminjaman mendukung kelancaran proses belajar mengajar di Program Studi Informatika Universitas Tanjungpura.",
"requirements": [
"Melampirkan surat permohonan peminjaman laboratorium",
"Terdapat dosen atau asisten praktikum yang bertanggung jawab",
"Pengguna menaati tata tertib dan ketentuan penggunaan perangkat laboratorium"
],
"procedures": [
"Dosen atau mahasiswa mengajukan surat permohonan peminjaman laboratorium kepada pengelola",
"Pengguna menyampaikan jadwal penggunaan laboratorium",
"Pengelola menyusun jadwal penggunaan laboratorium",
"Laboratorium digunakan sesuai jadwal dan ketentuan yang berlaku"
],
"staff_responsible": [
"👤 Staf Laboratorium Informatika"
],
"additional_info": "Pengguna laboratorium wajib menjaga kebersihan, keamanan, dan kondisi perangkat laboratorium selama kegiatan berlangsung."
}
]
+43
View File
@@ -0,0 +1,43 @@
[
{
"id": 1,
"name": "Tuning Rahayu, S.Hut.",
"role": "Staf Administrasi Informatika",
"image": "assets/images/staff-female.png",
"location": "Ruang Administrasi Prodi Informatika",
"contact_consent": true,
"phone": "6282350250892",
"schedule": [
{
"day": "Senin Jumat",
"time": "07.3016.00 WIB",
"break": "istirahat 12.0013.00 WIB"
}
],
"tasks": [
"Administrasi mahasiswa",
"Informasi berkas akademik"
]
},
{
"id": 2,
"name": "Mahyus Safutra, A.Md.",
"role": "Staf Laboratorium Informatika",
"image": "assets/images/staff-male.png",
"location": "Laboratorium Informatika",
"contact_consent": true,
"phone": "895338672533",
"schedule": [
{
"day": "Senin Jumat",
"time": "07.3016.00 WIB",
"break": "istirahat 12.0013.00 WIB"
}
],
"tasks": [
"Pengelolaan laboratorium",
"Dukungan praktikum",
"Peminjaman ruangan dan alat"
]
}
]
+148 -16
View File
@@ -1,5 +1,6 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="id" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -7,8 +8,11 @@
<link rel="stylesheet" href="../../assets/global/output.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="./style.css">
<style>
/* ===== MOBILE NAV STABILITY FIX =====
Prevents logo bar from collapsing/shifting on Android when
@@ -293,9 +297,10 @@
}
</style>
</head>
<body class="text-slate-800 antialiased bg-white selection:bg-untan-yellow selection:text-black">
<a href="#main-content" class="skip-link" data-i18n="skip_to_content">Langsung ke Konten Utama</a>
<a href="#main-content" class="skip-link">Langsung ke Konten Utama</a>
<div id="nav-wrapper"
class="fixed top-0 left-0 right-0 z-50 transition-shadow duration-300 shadow-sm border-b border-slate-200 will-change-auto">
@@ -373,7 +378,7 @@
<div class="nav-menu-item relative" data-menu="tentang">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_about">Tentang Kami</span>
<span>Tentang Kami</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -382,7 +387,7 @@
<div class="nav-menu-item relative" data-menu="program">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_program">Program</span>
<span>Program</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -391,7 +396,7 @@
<div class="nav-menu-item relative" data-menu="sdm">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_sdm">SDM</span>
<span>SDM</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -400,7 +405,7 @@
<div class="nav-menu-item relative" data-menu="fasilitas">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_facilities">Fasilitas</span>
<span>Fasilitas</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -409,7 +414,7 @@
<div class="nav-menu-item relative" data-menu="karya">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_works">Karya & Agenda</span>
<span>Karya & Agenda</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -418,7 +423,7 @@
<div class="nav-menu-item relative" data-menu="kontak">
<button
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
<span data-i18n="nav_contact">Kontak</span>
<span>Kontak</span>
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
</button>
</div>
@@ -1142,15 +1147,141 @@
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
<div class="container mx-auto px-4 lg:px-8 py-12">
<div class="mb-8 pb-8 border-b border-slate-200">
<span class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas Halaman Statis</span>
<span
class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas
Halaman Statis</span>
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Judul Halaman Group 04</h1>
<p class="text-slate-500 text-sm">Kelompok 04 &mdash; C4A &nbsp;&middot;&nbsp; Informatika UNTAN 2026</p>
<p class="text-slate-500 text-sm">Kelompok 04 &mdash; C4A &nbsp;&middot;&nbsp; Informatika UNTAN 2026
</p>
</div>
<div id="assignment-body" class="prose prose-slate max-w-none">
<!-- ================= START: BAGIAN YANG DIEDIT KELOMPOK ================= -->
<p>Tulis konten halaman kelompok di sini.</p>
<h3>Subjudul</h3>
<p>Gunakan paragraf, list, tabel, gambar, dan elemen konten lainnya.</p>
<!-- HERO SECTION -->
<section class="c4a-hero reveal">
<div class="c4a-hero-inner">
<div class="c4a-hero-tag" data-i18n="grp_c4a_hero_tag">LAYANAN AKADEMIK &bull; INFORMATIKA UNTAN
</div>
<h1 class="c4a-hero-title" data-i18n-html="grp_c4a_hero_title">
Staf Pendukung &amp; <br/>
<span class="highlight-orange">Standar Operasional Prosedur (SOP)</span>
</h1>
<p class="c4a-hero-desc" data-i18n="grp_c4a_hero_desc">Halaman ini menampilkan informasi petugas
layanan akademik serta
prosedur administrasi
mahasiswa
Program Studi Informatika UNTAN.</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-number" id="stat-staf">0</div>
<div class="stat-label" data-i18n="grp_c4a_stat_staf">Staf Aktif</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-number" id="stat-sop">0</div>
<div class="stat-label" data-i18n="grp_c4a_stat_sop">Jenis SOP</div>
</div>
</div>
</div>
</section>
<!-- STAFF SECTION -->
<section class="content-section">
<div class="section-header">
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_staff_tag">Tim
Kami</span></div>
<h2 class="section-title" data-i18n="grp_c4a_staff_title">Staf Pendukung</h2>
<p class="section-subtitle" data-i18n="grp_c4a_staff_subtitle">Staf yang siap melayani kebutuhan
akademik dan operasional
laboratorium di Prodi Informatika UNTAN.</p>
</div>
<div class="staff-grid" id="staff-container">
<!-- Kartu staf akan diisi secara dinamis melalui main.js berdasarkan data-staff.json -->
</div>
</section>
<!-- SOP SECTION -->
<section class="sop-section">
<div class="section-header">
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_sop_tag">Prosedur
Resmi</span></div>
<h2 class="section-title" data-i18n="grp_c4a_sop_title">Standar Operasional Prosedur (SOP)</h2>
<p class="section-subtitle" data-i18n="grp_c4a_sop_subtitle">Standar Operasional Prosedur untuk
berbagai kebutuhan administrasi
akademik mahasiswa. Gunakan pencarian atau filter untuk menemukan SOP yang Anda butuhkan.
</p>
</div>
<!-- SEARCH & FILTER BAR -->
<div class="sop-toolbar reveal">
<div class="sop-search-wrap">
<i class="fas fa-search sop-search-icon"></i>
<input type="search" id="sop-search-input" class="sop-search-input"
data-i18n-placeholder="grp_c4a_sop_search_placeholder"
placeholder="Cari SOP berdasarkan judul..." autocomplete="off" aria-label="Cari SOP" />
<button class="sop-search-clear" id="sop-search-clear" aria-label="Hapus pencarian"
style="display:none;">
<i class="fas fa-times"></i>
</button>
</div>
<div class="sop-filter-chips" id="sop-filter-chips" role="group"
aria-label="Filter kategori SOP">
<button class="sop-chip active" data-filter="semua" id="chip-semua" data-i18n="grp_c4a_sop_chip_semua">Semua</button>
<button class="sop-chip" data-filter="akademik" id="chip-akademik">
<i class="fas fa-graduation-cap"></i> <span data-i18n="grp_c4a_sop_chip_akademik">Akademik</span>
</button>
<button class="sop-chip" data-filter="praktikum" id="chip-praktikum">
<i class="fas fa-flask"></i> <span data-i18n="grp_c4a_sop_chip_praktikum">Praktikum</span>
</button>
</div>
</div>
<!-- RESULT COUNT -->
<div class="sop-result-info" id="sop-result-info" aria-live="polite"></div>
<!-- SOP CARD GRID -->
<div class="sop-card-grid" id="sop-cards-container"></div>
<!-- EMPTY STATE -->
<div class="sop-empty-state" id="sop-empty-state" style="display:none;">
<i class="fas fa-search-minus sop-empty-icon"></i>
<p class="sop-empty-title" data-i18n="grp_c4a_sop_empty_title">SOP tidak ditemukan</p>
<p class="sop-empty-desc" data-i18n="grp_c4a_sop_empty_desc">Coba kata kunci lain atau reset
filter pencarian.</p>
<button class="sop-empty-reset" id="sop-empty-reset">
<i class="fas fa-undo"></i> <span data-i18n="grp_c4a_sop_reset_btn">Reset Pencarian</span>
</button>
</div>
<!-- SOP DETAIL MODAL -->
<div class="sop-modal-overlay" id="sop-modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="sop-modal-title">
<div class="sop-modal-box" id="sop-modal-box">
<button class="sop-modal-close" id="sop-modal-close" aria-label="Tutup detail SOP">
<i class="fas fa-times"></i>
</button>
<div id="sop-modal-content"></div>
</div>
</div>
</section>
<!-- FAQ SECTION -->
<section class="faq-section-wrap">
<div class="faq-section-inner">
<div class="faq-header reveal">
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_faq_tag">Pertanyaan Umum</span></div>
<h2 data-i18n="grp_c4a_faq_title">Pertanyaan yang Sering Diajukan</h2>
<p data-i18n="grp_c4a_faq_subtitle">Temukan jawaban cepat atas pertanyaan umum mahasiswa
seputar layanan akademik prodi.</p>
</div>
<div class="faq-wrap" id="faq-container">
<!-- Item FAQ diisi secara dinamis melalui main.js berdasarkan data-faq.json -->
</div>
</div>
</section>
<script type="module" src="./js/main.js"></script>
<!-- ================= END: BAGIAN YANG DIEDIT KELOMPOK ================= -->
</div>
</div>
@@ -1215,8 +1346,8 @@
</div>
<div class="w-full relative z-0 mt-4 pointer-events-none flex justify-center">
<img src="../../assets/global/footer.png" alt="Footer Illustration" class="w-full h-auto object-cover object-bottom"
style="object-position: bottom;">
<img src="../../assets/global/footer.png" alt="Footer Illustration"
class="w-full h-auto object-cover object-bottom" style="object-position: bottom;">
</div>
</footer>
@@ -1335,4 +1466,5 @@
<script src="../../assets/global/localization.js"></script>
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
/**
* Berisi fungsi-fungsi untuk mengambil data (Data Fetching).
*/
/**
* Mengambil data staf pendukung dari berkas JSON.
* @returns {Promise<Array<Object>>} Daftar objek data staf
*/
export async function getStaffData() {
try {
const response = await fetch('data/data-staff.json');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Gagal memuat data staf:', error);
return [];
}
}
/**
* Mengambil data SOP dari berkas JSON.
* @returns {Promise<Array<Object>>} Daftar objek data SOP
*/
export async function getSopData() {
try {
const response = await fetch('data/data-sop.json');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Gagal memuat data SOP:', error);
return [];
}
}
/**
* Mengambil data FAQ dari berkas JSON.
* @returns {Promise<Array<Object>>} Daftar objek data FAQ
*/
export async function getFaqData() {
try {
const response = await fetch('data/data-faq.json');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Gagal memuat data FAQ:', error);
return [];
}
}
+71
View File
@@ -0,0 +1,71 @@
/**
* Main Entry Point Kelompok C4A.
* Menghubungkan pengambilan data dengan rendering UI dan interaksi halaman.
*/
import { getStaffData, getSopData, getFaqData } from './data.js';
import {
renderStaffCards,
renderSopCards,
renderFaqSection,
initSopInteraction,
initAnimations,
initFaq,
applySopFilter
} from './ui.js';
// Cache data secara global untuk mendukung rendering UI
let cachedStaffData = [];
let cachedSopData = [];
let cachedFaqData = [];
/**
* Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel,
* merender konten awal, serta mendaftarkan event listener interaksi UI.
* @returns {Promise<void>}
*/
async function initApp() {
try {
const [staffData, sopData, faqData] = await Promise.all([
getStaffData(),
getSopData(),
getFaqData()
]);
cachedStaffData = staffData || [];
cachedSopData = sopData || [];
cachedFaqData = faqData || [];
renderStaffCards(cachedStaffData);
renderSopCards(cachedSopData);
renderFaqSection(cachedFaqData);
initSopInteraction();
initFaq();
// Memberikan sedikit waktu tunda (jeda) agar rendering DOM selesai sebelum memulai animasi.
setTimeout(() => {
initAnimations();
}, 100);
} catch (error) {
console.error('Terjadi kesalahan saat menginisialisasi aplikasi C4A:', error);
}
}
// Re-render UI saat bahasa berubah (data tetap sama, hanya label UI yang berganti via t())
document.addEventListener('languageChanged', () => {
renderStaffCards(cachedStaffData);
renderSopCards(cachedSopData);
renderFaqSection(cachedFaqData);
initFaq();
applySopFilter();
// Re-inisialisasi observer agar elemen .reveal yang baru di-render mendapat class .visible
setTimeout(() => {
initAnimations();
}, 100);
});
// Jalankan inisialisasi aplikasi saat DOM siap
document.addEventListener('DOMContentLoaded', initApp);
+639
View File
@@ -0,0 +1,639 @@
// --- Helper Fungsi ---
/**
* Helper untuk menerjemahkan string berdasarkan key dari localization.js.
* Menggunakan fallback jika key tidak ditemukan atau localization.js tidak aktif.
*/
function t(key, fallback) {
if (window.t) {
const val = window.t(key);
if (val !== key) return val;
}
return fallback;
}
/**
* Normalisasi nomor telepon ke format standard e.g. 628...
* @param {string} phone - Nomor telepon mentah
* @returns {string} Nomor telepon ternormalisasi
*/
function normalizePhone(phone) {
let digits = String(phone || '').replace(/\D/g, '');
if (digits.startsWith('0')) {
digits = `62${digits.slice(1)}`;
} else if (digits.startsWith('8')) {
digits = `62${digits}`;
}
return digits;
}
/**
* Format nomor telepon untuk tampilan UI
* @param {string} phone - Nomor telepon mentah
* @returns {string} Nomor telepon terformat (e.g. +62 8xx-xxxx-xxxx)
*/
function formatPhoneForDisplay(phone) {
const norm = normalizePhone(phone);
if (!norm) return '-';
return `+${norm.slice(0, 2)} ${norm.slice(2, 5)}-${norm.slice(5, 9)}-${norm.slice(9)}`;
}
/**
* Memvalidasi apakah nomor telepon valid untuk digunakan di WhatsApp.
* @param {string} phone - Nomor telepon mentah
* @returns {boolean} True jika nomor valid (seluler Indonesia 10-15 digit)
*/
function isValidWhatsAppNumber(phone) {
const norm = normalizePhone(phone);
return norm.startsWith('628') && norm.length >= 10 && norm.length <= 15;
}
/**
* Enkripsi nomor telepon menggunakan Base64 untuk keamanan kontak.
* @param {string} phone - Nomor telepon ternormalisasi
* @returns {string} Nomor telepon terenkripsi Base64
*/
function encodePhone(phone) {
return btoa(normalizePhone(phone));
}
/**
* Dekripsi nomor telepon dari Base64.
* @param {string} encoded - Nomor telepon terenkripsi Base64
* @returns {string} Nomor telepon terdekripsi
*/
function decodePhone(encoded) {
try {
return atob(encoded);
} catch (error) {
console.error('Gagal mendekripsi nomor telepon:', error);
return '';
}
}
/**
* Merender daftar staf pendukung ke dalam container HTML.
* @param {Array<Object>} staffs - Array data staf pendukung
*/
export function renderStaffCards(staffs) {
const container = document.getElementById('staff-container');
if (!container) return;
container.innerHTML = '';
if (!staffs || staffs.length === 0) {
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_staff', 'Data staf pendukung tidak tersedia saat ini.')}</p>`;
return;
}
staffs.forEach((staff, index) => {
const {
name,
role,
image,
location,
phone,
contact_consent: contactConsent,
schedule,
tasks
} = staff;
const scheduleHTML = schedule.map(sched =>
`<div class="schedule-item">` +
`<span class="s-day">${sched.day}</span>` +
`<span class="s-time">${sched.time} <span class="s-break">| ${sched.break}</span></span>` +
`</div>`
).join('');
const tasksHTML = tasks.map(task => `<li>${task}</li>`).join('');
const isPhoneValid = isValidWhatsAppNumber(phone);
const encodedPhone = isPhoneValid ? encodePhone(phone) : '';
const displayPhone = isPhoneValid ? formatPhoneForDisplay(phone) : '-';
const hideContactItemClass = contactConsent ? '' : 'is-hidden';
const hideContactActionClass = (contactConsent && isPhoneValid) ? '' : 'is-hidden';
const initial = name ? name.charAt(0).toUpperCase() : '';
const card = document.createElement('div');
card.className = `staff-card reveal reveal-delay-${index + 1}`;
card.innerHTML = `
<div class="staff-card-header">
<div class="staff-avatar">
<span class="staff-initial">${initial}</span>
<img src="${image}" alt="${name}" onerror="this.style.display='none'"/>
</div>
<div>
<div class="staff-name">${name}</div>
<div class="staff-role">${role}</div>
</div>
</div>
<div class="staff-card-body">
<div class="staff-info-grid">
<div class="staff-info-item"><span class="icon">📍</span><div><span class="label">${t('grp_c4a_label_location', 'Lokasi')}</span><span class="val">${location}</span></div></div>
<div class="staff-info-item">
<span class="icon">🕒</span>
<div style="flex:1">
<span class="label">${t('grp_c4a_label_service_hours', 'Jam Layanan')}</span>
<div class="schedule-list">${scheduleHTML}</div>
</div>
</div>
<div class="staff-info-item staff-contact-item ${hideContactItemClass}"><span class="icon">📞</span><div><span class="label">${t('grp_c4a_label_wa_contact', 'Kontak WhatsApp')}</span><span class="val">${displayPhone}</span></div></div>
</div>
<div class="staff-divider"></div>
<div class="staff-tasks-title">${t('grp_c4a_label_tasks', 'Tugas & Tanggung Jawab')}</div>
<ul class="staff-tasks">${tasksHTML}</ul>
</div>
<div class="staff-card-actions staff-contact-actions ${hideContactActionClass}">
<button class="btn-staff btn-staff-wa" data-wa="${encodedPhone}" aria-label="${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')} ${name}"><svg class="wa-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg> ${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')}</button>
</div>
`;
container.appendChild(card);
// Setup WA button click handler dengan dekrips nomor
const waBtn = card.querySelector('[data-wa]');
if (waBtn) {
waBtn.addEventListener('click', () => {
const phoneNum = decodePhone(waBtn.dataset.wa);
if (phoneNum) {
window.open(`https://wa.me/${phoneNum}`, '_blank', 'noopener,noreferrer');
}
});
}
});
}
// ============================================================
// --- Render SOP sebagai Card Grid dengan Search & Filter ---
// ============================================================
const CATEGORY_COLORS = {
akademik: 'sop-cat-akademik',
praktikum: 'sop-cat-praktikum',
};
/**
* Ambil label kategori
* @param {string} category - Kategori SOP ('akademik' atau 'praktikum')
* @returns {string} Label kategori
*/
function getCatLabel(category) {
const map = {
akademik: t('grp_c4a_sop_chip_akademik', 'Akademik'),
praktikum: t('grp_c4a_sop_chip_praktikum', 'Praktikum')
};
return map[category] || t('grp_c4a_sop_chip_akademik', 'Akademik');
}
/**
* Buat HTML untuk satu SOP card di grid.
* @param {Object} sop - Data objek SOP
* @returns {string} String markup HTML kartu SOP
*/
function buildSopCard(sop) {
const {
tab_id: tabId,
category,
icon = 'fa-file-alt',
title,
description,
procedures
} = sop;
const catLabel = getCatLabel(category);
const catClass = CATEGORY_COLORS[category] || '';
return `
<article class="sop-grid-card reveal" data-tab-id="${tabId}" data-category="${category}" data-title="${title.toLowerCase()}">
<div class="sgc-header">
<div class="sgc-icon-wrap">
<i class="fas ${icon}"></i>
</div>
<div class="sgc-meta">
<h3 class="sgc-title">${title}</h3>
</div>
</div>
<div class="sgc-category-row">
<span class="sgc-category ${catClass}">${catLabel}</span>
</div>
<p class="sgc-desc">${description}</p>
<div class="sgc-footer">
<div class="sgc-steps">
<i class="fas fa-list-ol"></i>
<span>${procedures.length} ${t('grp_c4a_label_steps', 'Langkah')}</span>
</div>
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="${t('grp_c4a_label_view_detail', 'Lihat Detail')} ${title}">
${t('grp_c4a_label_view_detail', 'Lihat Detail')} <i class="fas fa-arrow-right"></i>
</button>
</div>
</article>`;
}
/**
* Buat HTML untuk isi modal detail SOP.
* @param {Object} sop - Data objek SOP
* @returns {string} String markup HTML isi modal
*/
function buildSopModalContent(sop) {
const {
category,
icon = 'fa-file-alt',
title,
description,
purpose,
requirements,
procedures,
staff_responsible: staffResponsible,
additional_info: additionalInfo
} = sop;
const catLabel = getCatLabel(category);
const catClass = CATEGORY_COLORS[category] || '';
const reqsHTML = requirements.map(req => `<li>${req}</li>`).join('');
const procsHTML = procedures.map(proc => `<li>${proc}</li>`).join('');
// staffResponsible bersifat opsional — sembunyikan blok jika tidak ada
const hasStaff = Array.isArray(staffResponsible) && staffResponsible.length > 0;
const staffHTML = hasStaff
? staffResponsible.map(staffName => `<span class="sop-staff-pill">${staffName}</span>`).join('')
: '';
const staffBlockHTML = hasStaff
? `<div class="sop-staff-block">
<div class="sop-block-label">${t('grp_c4a_label_responsible_staff', 'Staf Bertanggung Jawab (Bersifat Opsional)')}</div>
<div class="sop-staff-pills">${staffHTML}</div>
</div>`
: '';
// additionalInfo bersifat opsional — sembunyikan blok jika tidak ada
const additionalInfoHTML = additionalInfo
? `<div class="sop-info-box">
<span class="iico">️</span>
<p class="sop-info-box-text"><strong>${t('grp_c4a_label_additional_info', 'Informasi Tambahan (Bersifat Opsional):')}</strong> ${additionalInfo}</p>
</div>`
: '';
return `
<div class="sop-modal-header">
<div class="sop-modal-header-top">
<div class="sop-modal-icon-wrap">
<i class="fas ${icon}"></i>
</div>
<div>
<div class="sop-modal-badges">
<span class="sgc-category ${catClass}">${catLabel}</span>
</div>
<h2 class="sop-modal-title" id="sop-modal-title">${title}</h2>
<p class="sop-modal-desc">${description}</p>
</div>
</div>
</div>
<div class="sop-modal-body">
<div class="sop-block">
<div class="sop-block-label">${t('grp_c4a_label_purpose', 'Tujuan')}</div>
<p class="sop-block-text">${purpose}</p>
</div>
<div class="sop-two-col">
<div>
<div class="sop-block-label">${t('grp_c4a_label_requirements', 'Persyaratan')}</div>
<ul class="sop-list bullets">${reqsHTML}</ul>
</div>
<div>
<div class="sop-block-label">${t('grp_c4a_label_procedure', 'Alur Prosedur')}</div>
<ul class="sop-list numbered">${procsHTML}</ul>
</div>
</div>
${staffBlockHTML}
${additionalInfoHTML}
</div>`;
}
/**
* Render semua SOP card ke grid.
* @param {Array<Object>} sops - Array objek SOP
*/
export function renderSopCards(sops) {
const container = document.getElementById('sop-cards-container');
if (!container) return;
// simpan data global di element agar bisa diakses filter/search
container._sopData = sops;
// Tampilkan pesan jika data SOP kosong atau gagal dimuat
if (!sops || sops.length === 0) {
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_sop', 'Data SOP tidak tersedia saat ini.')}</p>`;
updateResultInfo(0, 0);
return;
}
container.innerHTML = sops.map(buildSopCard).join('');
updateResultInfo(sops.length, sops.length);
}
/** Update teks info jumlah hasil */
function updateResultInfo(shown, total) {
const el = document.getElementById('sop-result-info');
if (!el) return;
if (shown === total) {
const allTemplate = t('grp_c4a_sop_showing_all', 'Menampilkan {total} SOP');
el.textContent = allTemplate.replace('{total}', total);
} else {
const filteredTemplate = t('grp_c4a_sop_showing_filtered', 'Menampilkan {shown} dari {total} SOP');
el.textContent = filteredTemplate.replace('{shown}', shown).replace('{total}', total);
}
}
/**
* Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif.
*/
export function applySopFilter() {
const container = document.getElementById('sop-cards-container');
if (!container) return;
const query = (document.getElementById('sop-search-input')?.value || '').toLowerCase().trim();
const activeChip = document.querySelector('.sop-chip.active');
const activeFilter = activeChip ? activeChip.dataset.filter : 'semua';
const cards = container.querySelectorAll('.sop-grid-card');
let visibleCount = 0;
cards.forEach(card => {
const title = card.dataset.title || '';
const code = card.dataset.code || '';
const category = card.dataset.category || '';
const matchesSearch = !query || title.includes(query) || code.includes(query);
const matchesFilter = activeFilter === 'semua' || category === activeFilter;
const isVisible = matchesSearch && matchesFilter;
card.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
const emptyState = document.getElementById('sop-empty-state');
if (emptyState) emptyState.style.display = visibleCount === 0 ? 'flex' : 'none';
updateResultInfo(visibleCount, cards.length);
}
/**
* Membuka modal detail untuk SOP tertentu berdasarkan tab_id.
* @param {string} tabId - ID unik tab/SOP yang ingin ditampilkan
*/
function openSopModal(tabId) {
const container = document.getElementById('sop-cards-container');
if (!container || !container._sopData) return;
const sop = container._sopData.find(item => item.tab_id === tabId);
if (!sop) return;
const modalContent = document.getElementById('sop-modal-content');
const modalOverlay = document.getElementById('sop-modal-overlay');
if (!modalContent || !modalOverlay) return;
modalContent.innerHTML = buildSopModalContent(sop);
modalOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
// Gulir isi modal ke paling atas
const modalBox = document.getElementById('sop-modal-box');
if (modalBox) modalBox.scrollTop = 0;
}
/**
* Menutup modal detail SOP yang sedang aktif.
*/
function closeSopModal() {
const modalOverlay = document.getElementById('sop-modal-overlay');
if (modalOverlay) modalOverlay.classList.remove('active');
document.body.style.overflow = '';
}
/**
* Menginisialisasi interaksi halaman SOP (pencarian, filter chip, modal detail).
*/
export function initSopInteraction() {
// Search input
const searchInput = document.getElementById('sop-search-input');
const searchClear = document.getElementById('sop-search-clear');
if (searchInput) {
searchInput.addEventListener('input', () => {
if (searchClear) {
searchClear.style.display = searchInput.value ? 'flex' : 'none';
}
applySopFilter();
});
}
if (searchClear) {
searchClear.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
}
searchClear.style.display = 'none';
applySopFilter();
});
}
// Filter chips
document.querySelectorAll('.sop-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.sop-chip').forEach(activeChip => {
activeChip.classList.remove('active');
});
chip.classList.add('active');
applySopFilter();
});
});
// Empty state reset button
const emptyReset = document.getElementById('sop-empty-reset');
if (emptyReset) {
emptyReset.addEventListener('click', () => {
if (searchInput) {
searchInput.value = '';
}
if (searchClear) {
searchClear.style.display = 'none';
}
document.querySelectorAll('.sop-chip').forEach(activeChip => {
activeChip.classList.remove('active');
});
const allChip = document.querySelector('[data-filter="semua"]');
if (allChip) {
allChip.classList.add('active');
}
applySopFilter();
});
}
// Card detail button (event delegation)
const container = document.getElementById('sop-cards-container');
if (container) {
container.addEventListener('click', (event) => {
const btn = event.target.closest('.sgc-detail-btn');
if (btn) {
openSopModal(btn.dataset.tabId);
}
});
}
// Modal close button
const closeBtn = document.getElementById('sop-modal-close');
if (closeBtn) {
closeBtn.addEventListener('click', closeSopModal);
}
// Close on overlay click
const overlay = document.getElementById('sop-modal-overlay');
if (overlay) {
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
closeSopModal();
}
});
}
// Close on Escape key
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeSopModal();
}
});
}
// --- Kompatibilitas Mundur ---
/**
* Stub kompatibilitas mundur untuk renderSopTabs.
* @param {Array<Object>} sops - Array objek SOP
*/
export function renderSopTabs(sops) {
renderSopCards(sops);
}
/**
* Stub kompatibilitas mundur untuk initSopTabsInteraction.
*/
export function initSopTabsInteraction() {
initSopInteraction();
}
// --- Interaksi & Animasi ---
/**
* Menginisialisasi animasi scroll reveal dan counter statistik pada hero section.
*/
export function initAnimations() {
// Scroll Reveal
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add('visible');
revealObserver.unobserve(entry.target);
});
}, { threshold: 0.1 });
document.querySelectorAll('.reveal').forEach((el) => revealObserver.observe(el));
// Hero Counter
const COUNT_ANIMATION_DURATION_MS = 800;
/**
* Menjalankan animasi hitung angka (counter)
* @param {Element} targetElement - Elemen target angka
* @param {number} targetValue - Nilai akhir counter
*/
function animateCount(targetElement, targetValue) {
if (!targetElement) return;
const stepValue = targetValue / (COUNT_ANIMATION_DURATION_MS / 16);
let currentValue = 0;
const timerId = setInterval(() => {
currentValue += stepValue;
if (currentValue >= targetValue) {
currentValue = targetValue;
clearInterval(timerId);
}
targetElement.textContent = Math.floor(currentValue).toString();
}, 16);
}
const staffCounter = document.getElementById('stat-staf');
const sopCounter = document.getElementById('stat-sop');
// Hitung jumlah data yang berhasil di-render (tidak termasuk template)
const totalStaffCards = document.querySelectorAll('.staff-card').length;
const totalSopCards = document.querySelectorAll('.sop-grid-card').length;
animateCount(staffCounter, totalStaffCards);
animateCount(sopCounter, totalSopCards);
}
// --- FAQ Accordion ---
/**
* Membuka item FAQ yang diklik; menutup item lain yang sebelumnya
* terbuka (perilaku accordion satu-terbuka-sekaligus).
* @param {Element} questionEl - Elemen .faq-question yang diklik
*/
function toggleFaq(questionEl) {
const item = questionEl.closest('.faq-item');
if (!item) return;
const isOpen = item.classList.contains('open');
// Tutup semua item terlebih dahulu
document.querySelectorAll('.faq-item').forEach(faqItem => {
faqItem.classList.remove('open');
});
// Buka item yang diklik hanya jika sebelumnya tertutup
if (!isOpen) {
item.classList.add('open');
}
}
/**
* Menginisialisasi event listener klik untuk setiap accordion FAQ.
*/
export function initFaq() {
document.querySelectorAll('.faq-question').forEach(questionEl => {
questionEl.addEventListener('click', () => toggleFaq(questionEl));
});
}
// --- Render FAQ ---
/**
* Merender section FAQ secara dinamis dari data-faq.json.
* Diinjeksikan ke dalam #faq-container di index.html.
* @param {Array<Object>} faqs - Array objek FAQ dari data-faq.json
*/
export function renderFaqSection(faqs) {
const container = document.getElementById('faq-container');
if (!container) return;
if (!faqs || faqs.length === 0) {
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_faq', 'Data FAQ tidak tersedia saat ini.')}</p>`;
return;
}
const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3'];
const itemsHTML = faqs.map((faq, index) => {
const delayClass = delayClasses[index] || '';
return `
<div class="faq-item reveal ${delayClass}">
<div class="faq-question">
<span class="faq-question-text">${faq.question}</span>
<div class="faq-icon">+</div>
</div>
<div class="faq-answer">
<div class="faq-answer-inner">${faq.answer}</div>
</div>
</div>`;
}).join('');
container.innerHTML = itemsHTML;
}
+1487 -2
View File
File diff suppressed because it is too large Load Diff