diff --git a/groups/C4A/assets/images/staff-female.png b/groups/C4A/assets/images/staff-female.png new file mode 100644 index 0000000..11b08da Binary files /dev/null and b/groups/C4A/assets/images/staff-female.png differ diff --git a/groups/C4A/assets/images/staff-male.png b/groups/C4A/assets/images/staff-male.png new file mode 100644 index 0000000..bb57ea3 Binary files /dev/null and b/groups/C4A/assets/images/staff-male.png differ diff --git a/groups/C4A/data/data-faq.json b/groups/C4A/data/data-faq.json new file mode 100644 index 0000000..61f70e1 --- /dev/null +++ b/groups/C4A/data/data-faq.json @@ -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 pembayaran UKT, mengisi KRS, dan terdaftar di kelas Edlink." + }, + { + "id": 2, + "question": "Berapa persentase kehadiran minimal agar bisa mengikuti Ujian Akhir Semester (UAS)?", + "answer": "Mahasiswa wajib memenuhi minimal 75% kehadiran 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 mengakses hasil studi 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 100 SKS dan memiliki Indeks Prestasi Kumulatif (IPK) â‰Ĩ 2,00." + }, + { + "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 Penulisan Proposal Tugas Akhir dengan nilai minimal C. 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 S1 atau D4." + }, + { + "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 Tugas Akhir 1 (TA1) dan Tugas Akhir 2 (TA2) 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 120 SKS, memiliki IPK minimal 2,00, telah lulus mata kuliah Penulisan Proposal Tugas Akhir dengan nilai minimal C, serta telah mendaftarkan mata kuliah skripsi pada KRS." + }, + { + "id": 9, + "question": "Siapa yang berhak mendaftar menjadi Asisten Praktikum (Aslab)?", + "answer": "Rekrutmen terbuka bagi mahasiswa aktif Program Studi Informatika UNTAN 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 surat permohonan peminjaman kepada staf laboratorium pengelola dan menyampaikan jadwal penggunaannya. Penggunaan lab juga harus didampingi oleh dosen atau asisten praktikum yang bertanggung jawab." + } +] \ No newline at end of file diff --git a/groups/C4A/data/data-sop.json b/groups/C4A/data/data-sop.json new file mode 100644 index 0000000..5c912b4 --- /dev/null +++ b/groups/C4A/data/data-sop.json @@ -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." + } +] \ No newline at end of file diff --git a/groups/C4A/data/data-staff.json b/groups/C4A/data/data-staff.json new file mode 100644 index 0000000..be7f7fa --- /dev/null +++ b/groups/C4A/data/data-staff.json @@ -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.30–16.00 WIB", + "break": "istirahat 12.00–13.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.30–16.00 WIB", + "break": "istirahat 12.00–13.00 WIB" + } + ], + "tasks": [ + "Pengelolaan laboratorium", + "Dukungan praktikum", + "Peminjaman ruangan dan alat" + ] + } +] \ No newline at end of file diff --git a/groups/C4A/index.html b/groups/C4A/index.html index 01a6da5..c89bb6f 100644 --- a/groups/C4A/index.html +++ b/groups/C4A/index.html @@ -1,5 +1,6 @@ īģŋ + @@ -7,9 +8,12 @@ - + - + - + -
+
- Tugas Halaman Statis -

Judul Halaman Group 04

-

Kelompok 04 — C4A  ·  Informatika UNTAN 2026

+ Tugas + Halaman Statis +

Staf Pendukung dan Standar Operasional + Prosedur (SOP)

+

Kelompok 04 — C4A  ·  Informatika UNTAN 2026 +

-

Tulis konten halaman kelompok di sini.

-

Subjudul

-

Gunakan paragraf, list, tabel, gambar, dan elemen konten lainnya.

+ +
+
+
LAYANAN AKADEMIK • INFORMATIKA UNTAN +
+

+ Staf Pendukung &
+ Standar Operasional Prosedur (SOP) +

+

Halaman ini menampilkan informasi petugas + layanan akademik serta + prosedur administrasi + mahasiswa + Program Studi Informatika UNTAN.

+
+
+
0
+
Staf Aktif
+
+
+
+
0
+
Jenis SOP
+
+
+
+
+ + +
+
+ +

Staf Pendukung

+

Staf yang siap melayani kebutuhan + akademik dan operasional + laboratorium di Prodi Informatika UNTAN.

+
+
+ +
+
+ + +
+
+ +

Standar Operasional Prosedur (SOP)

+

Standar Operasional Prosedur untuk + berbagai kebutuhan administrasi + akademik mahasiswa. Gunakan pencarian atau filter untuk menemukan SOP yang Anda butuhkan. +

+
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+ + +
+ + + + + + +
+ + +
+
+
+ +

Pertanyaan yang Sering Diajukan

+

Temukan jawaban cepat atas pertanyaan umum mahasiswa + seputar layanan akademik prodi.

+
+ +
+ +
+
+
+ +
@@ -1215,8 +1345,8 @@
- Footer Illustration + Footer Illustration
@@ -1312,27 +1442,28 @@ })(); - + - + + \ No newline at end of file diff --git a/groups/C4A/js/data.js b/groups/C4A/js/data.js new file mode 100644 index 0000000..d30e302 --- /dev/null +++ b/groups/C4A/js/data.js @@ -0,0 +1,54 @@ +/** + * Berisi fungsi-fungsi untuk mengambil data (Data Fetching). + */ + +/** + * Mengambil data staf pendukung dari berkas JSON. + * @returns {Promise>} 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>} 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>} 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 []; + } +} diff --git a/groups/C4A/js/main.js b/groups/C4A/js/main.js new file mode 100644 index 0000000..8cee83e --- /dev/null +++ b/groups/C4A/js/main.js @@ -0,0 +1,56 @@ +/** + * 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 +} from './ui.js'; + +// Cache data secara global untuk mendukung rendering UI +let cachedStaffData = []; +let cachedSopData = []; + +/** + * Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel, + * merender konten awal, serta mendaftarkan event listener interaksi UI. + * @returns {Promise} + */ +async function initApp() { + try { + const [staffData, sopData, faqData] = await Promise.all([ + getStaffData(), + getSopData(), + getFaqData() + ]); + + cachedStaffData = staffData || []; + cachedSopData = sopData || []; + + renderStaffCards(cachedStaffData); + renderSopCards(cachedSopData); + renderFaqSection(faqData || []); + + 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); + } +} + +// NOTE: Fitur bilingual sudah dihapus - languageChanged listener tidak lagi diperlukan +// Silakan hapus atau archive jika ada implementasi global untuk bahasa + +// Jalankan inisialisasi aplikasi saat DOM siap +document.addEventListener('DOMContentLoaded', initApp); diff --git a/groups/C4A/js/ui.js b/groups/C4A/js/ui.js new file mode 100644 index 0000000..193ed38 --- /dev/null +++ b/groups/C4A/js/ui.js @@ -0,0 +1,576 @@ +// --- Helper Fungsi --- + +/** + * 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; +} + +/** + * Merender daftar staf pendukung ke dalam container HTML. + * @param {Array} 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 = `

Data staf pendukung tidak tersedia saat ini.

`; + return; + } + + staffs.forEach((staff, index) => { + const { + name, + role, + image, + location, + phone, + schedule, + tasks + } = staff; + + const scheduleHTML = schedule.map(sched => + `
` + + `${sched.day}` + + `${sched.time} | ${sched.break}` + + `
` + ).join(''); + + const tasksHTML = tasks.map(task => `
  • ${task}
  • `).join(''); + const initial = name ? name.charAt(0).toUpperCase() : ''; + + const card = document.createElement('div'); + card.className = `staff-card reveal reveal-delay-${index + 1}`; + + card.innerHTML = ` +
    +
    + ${initial} + ${name} +
    +
    +
    ${name}
    +
    ${role}
    +
    +
    +
    +
    +
    📍
    Lokasi${location}
    +
    + 🕒 +
    + Jam Layanan +
    ${scheduleHTML}
    +
    +
    +
    📞
    Kontak${phone}
    +
    +
    +
    Tugas & Tanggung Jawab
    +
      ${tasksHTML}
    +
    + `; + + container.appendChild(card); + }); +} +// ============================================================ +// --- 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: 'Akademik', praktikum: 'Praktikum' }; + return map[category] || '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 ` +
    +
    +
    + +
    +
    +

    ${title}

    +
    +
    +
    + ${catLabel} +
    +

    ${description}

    + +
    `; +} + +/** + * 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 => `
  • ${req}
  • `).join(''); + const procsHTML = procedures.map(proc => `
  • ${proc}
  • `).join(''); + + // staffResponsible bersifat opsional — sembunyikan blok jika tidak ada + const hasStaff = Array.isArray(staffResponsible) && staffResponsible.length > 0; + const staffHTML = hasStaff + ? staffResponsible.map(staffName => `${staffName}`).join('') + : ''; + const staffBlockHTML = hasStaff + ? `
    +
    Staf Bertanggung Jawab (Bersifat Opsional)
    +
    ${staffHTML}
    +
    ` + : ''; + + // additionalInfo bersifat opsional — sembunyikan blok jika tidak ada + const additionalInfoHTML = additionalInfo + ? `
    + â„šī¸ +

    Informasi Tambahan (Bersifat Opsional): ${additionalInfo}

    +
    ` + : ''; + + return ` +
    +
    +
    + +
    +
    +
    + ${catLabel} +
    +

    ${title}

    +

    ${description}

    +
    +
    +
    +
    +
    +
    Tujuan
    +

    ${purpose}

    +
    +
    +
    +
    Persyaratan
    +
      ${reqsHTML}
    +
    +
    +
    Alur Prosedur
    +
      ${procsHTML}
    +
    +
    + ${staffBlockHTML} + ${additionalInfoHTML} +
    `; +} + +/** + * Render semua SOP card ke grid. + * @param {Array} 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 = `

    Data SOP tidak tersedia saat ini.

    `; + 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) { + el.textContent = `Menampilkan ${total} SOP`; + } else { + el.textContent = `Menampilkan ${shown} dari ${total} SOP`; + } +} + +/** + * Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif. + */ +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} 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} 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 = `

    Data FAQ tidak tersedia saat ini.

    `; + return; + } + + const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3']; + + const itemsHTML = faqs.map((faq, index) => { + const delayClass = delayClasses[index] || ''; + return ` +
    +
    + ${faq.question} +
    +
    +
    +
    +
    ${faq.answer}
    +
    +
    `; + }).join(''); + + container.innerHTML = itemsHTML; +} \ No newline at end of file diff --git a/groups/C4A/style.css b/groups/C4A/style.css index c31a92b..319e3eb 100644 --- a/groups/C4A/style.css +++ b/groups/C4A/style.css @@ -5,11 +5,13 @@ --text-soft: #64748b; --bg-soft: #f8fafc; --line: #e2e8f0; + --font-primary: "Roboto", Arial, sans-serif; + --font-accent: "Roboto Condensed", "Roboto", sans-serif; } * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } -body { font-family: "Roboto", Arial, sans-serif; color: var(--text-main); background: #fff; line-height: 1.6; } +body { font-family: var(--font-primary); color: var(--text-main); background: #fff; line-height: 1.6; } .container { width: min(1100px, 92%); margin-inline: auto; } .nav { background: var(--brand-navy); color: #fff; border-bottom: 4px solid var(--brand-yellow); padding: 14px 0; } @@ -20,7 +22,41 @@ body { font-family: "Roboto", Arial, sans-serif; color: var(--text-main); backgr .hero-inner { position: relative; z-index: 1; padding: 36px 0; } .badge { display: inline-block; background: var(--brand-yellow); color: #111827; font-size: .68rem; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; padding: .2rem .45rem; margin-bottom: 12px; } .hero h1 { margin: 0; font-size: clamp(1.6rem, 3.2vw, 2.8rem); line-height: 1.2; } - +.hero-desc { + margin: 16px 0 0; + font-size: clamp(0.95rem, 1.8vw, 1.1rem); + line-height: 1.6; + max-width: 700px; + color: rgba(255, 255, 255, 0.9); +} +/* Hero Stats */ +.hero-stats { + display: flex; + align-items: center; + gap: 32px; + margin-top: 32px; +} +.stat-item { + text-align: left; +} +.stat-number { + font-size: clamp(2rem, 4vw, 3rem); + font-weight: 700; + color: var(--brand-yellow); + line-height: 1; + margin-bottom: 8px; +} +.stat-label { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.85); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.stat-divider { + width: 2px; + height: 50px; + background: rgba(255, 255, 255, 0.2); +} .layout { display: grid; grid-template-columns: 1fr; gap: 2rem; padding: 2rem 0 4rem; } @media (min-width: 1024px) { .layout { grid-template-columns: minmax(0, 3fr) minmax(260px, 1fr); } } @@ -45,3 +81,1452 @@ body { font-family: "Roboto", Arial, sans-serif; color: var(--text-main); backgr .footer-inner { display: flex; align-items: center; justify-content: center; gap: .75rem; padding: 1rem 0; } .footer-logo { height: 28px; width: auto; } .footer-ornament { width: 100%; display: block; opacity: .9; } + +/* ========================================= */ +/* CSS KUSTOM KELOMPOK C4A (STAF & SOP) */ +/* ========================================= */ + +:root { + --navy: #003150; + --navy-light: #004a73; + --gold: #feb401; + --gold-light: #ffc933; + --gold-pale: #fff8e6; + --shadow-sm: 0 2px 12px rgba(0, 49, 80, .08); + --shadow-md: 0 8px 32px rgba(0, 49, 80, .14); + --shadow-lg: 0 20px 60px rgba(0, 49, 80, .18); + --radius: 12px; + --radius-sm: 8px; + --transition: .28s cubic-bezier(.4, 0, .2, 1); +} + +/* HERO SECTION CUSTOM */ +.c4a-hero { + background: linear-gradient(135deg, var(--navy) 0%, #001a2b 100%); + width: 100vw; + position: relative; + left: 50%; + right: 50%; + margin-left: -50vw; + margin-right: -50vw; + border-radius: 0; + padding: 60px 0; + color: #fff; + margin-bottom: 50px; + overflow: hidden; + box-shadow: var(--shadow-md); +} + +.c4a-hero::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--gold), var(--navy-light)); +} + +.c4a-hero::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 85% 15%, rgba(254, 180, 1, 0.1) 0%, transparent 60%); + pointer-events: none; +} + +.c4a-hero-inner { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; +} + +@media (min-width: 1024px) { + .c4a-hero-inner { + padding: 0 32px; + } +} + +.c4a-hero-tag { + color: var(--gold) !important; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +#assignment-body .c4a-hero-title { + font-family: var(--font-accent); + font-size: clamp(2.2rem, 4.5vw, 3.4rem); + font-weight: 800; + color: #fff !important; + margin: 0 0 16px !important; + line-height: 1.15; + letter-spacing: -0.5px; +} + +#assignment-body .c4a-hero-title .highlight-orange { + color: var(--gold) !important; +} + +#assignment-body .c4a-hero-desc { + font-size: 15px; + color: rgba(255, 255, 255, 0.85) !important; + line-height: 1.65; + max-width: 720px; + margin: 0 0 32px !important; +} + +/* SLEEK MINI STATS */ +#assignment-body .hero-stats { + display: flex !important; + align-items: center !important; + gap: 24px !important; + margin-top: 28px !important; +} + +#assignment-body .stat-item { + text-align: left !important; +} + +#assignment-body .stat-number { + font-size: 26px !important; + font-weight: 800 !important; + color: var(--gold) !important; + margin-bottom: 2px !important; + font-family: var(--font-accent) !important; + line-height: 1 !important; +} + +#assignment-body .stat-label { + font-size: 10px !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; + color: rgba(255, 255, 255, 0.6) !important; + margin: 0 !important; +} + +#assignment-body .stat-divider { + width: 1px !important; + height: 28px !important; + background: rgba(255, 255, 255, 0.18) !important; + margin: 0 !important; +} + +@media (max-width: 640px) { + .c4a-hero { + padding: 40px 0; + } +} + +/* CONTENT SECTIONS */ +.content-section { + padding: 60px 0; +} + +.section-header { + margin-bottom: 40px; +} + +.section-tag { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--gold-pale); + color: #9d6b00; + padding: 5px 14px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 12px; +} + +.section-tag .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--gold); +} + +.section-title { + font-size: clamp(1.7rem, 3vw, 2.2rem); + font-weight: 700; + color: var(--navy); + line-height: 1.2; + margin: 0; +} + +.section-subtitle { + font-size: 16px; + color: var(--text-soft); + margin-top: 10px; + line-height: 1.6; +} + +/* STAFF CARDS */ +.staff-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 28px; +} + +.staff-card { + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: transform var(--transition), box-shadow var(--transition); + position: relative; + display: flex; + flex-direction: column; +} + +.staff-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--gold), var(--navy)); +} + +.staff-card:hover { + transform: translateY(-6px); + box-shadow: var(--shadow-lg); +} + +.staff-card-header { + background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%); + padding: 24px; + display: flex; + align-items: center; + gap: 16px; +} + +.staff-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background: linear-gradient(135deg, var(--gold) 0%, var(--gold-light) 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 700; + color: var(--navy); + flex-shrink: 0; + border: 3px solid rgba(255, 255, 255, .2); + overflow: hidden; + position: relative; +} + +.staff-avatar img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.staff-name { + font-size: 1.15rem; + font-weight: 700; + color: #fff; + margin: 0; +} + +.staff-role { + margin-top: 4px; + display: inline-block; + background: rgba(254, 180, 1, .22); + color: var(--gold-light); + padding: 3px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; +} + +.staff-card-body { + padding: 24px; + flex: 1; +} + +.staff-info-grid { + display: grid; + gap: 14px; + margin-bottom: 20px; +} + +.staff-info-item { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; +} + +.staff-info-item .icon { + font-size: 16px; + flex-shrink: 0; + margin-top: 2px; +} + +.staff-info-item .label { + display: block; + font-weight: 600; + color: var(--navy); + margin-bottom: 2px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.staff-info-item .val { + display: block; + color: var(--text-main); +} + +.schedule-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 4px; +} + +.schedule-item { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; +} + +.s-day { + font-weight: 600; + color: var(--navy); + font-size: 13px; +} + +.s-time { + color: var(--text-main); + font-size: 13px; +} + +.s-break { + color: var(--text-soft); + font-size: 12px; +} + +.staff-divider { + height: 1px; + background: var(--line); + margin: 20px 0; +} + +.staff-tasks-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--navy); + margin-bottom: 10px; +} + +.staff-tasks { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.staff-tasks li { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 14px; + color: var(--text-main); +} + +.staff-tasks li::before { + content: 'â€ĸ'; + color: var(--gold); + font-weight: 700; + flex-shrink: 0; +} + +.staff-card-actions { + padding: 16px 24px; + border-top: 1px solid var(--line); + display: flex; + gap: 10px; +} + +.btn-staff { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 18px; + border-radius: var(--radius-sm); + font-size: 13px; + font-weight: 600; + border: none; + cursor: pointer; + transition: var(--transition); + text-decoration: none; +} + +.btn-staff-wa { + background: var(--navy); + color: #fff !important; + border-radius: 8px; +} + +.btn-staff-wa:hover { + background: var(--navy-light); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.wa-icon { + width: 18px; + height: 18px; +} + +/* SOP SECTION */ +.sop-section { + padding: 60px 0; + background: transparent; + margin: 60px 0; + border-radius: 12px; +} + +.tab-nav-wrap { + margin-bottom: 32px; + border-bottom: 2px solid var(--line); +} + +.tab-nav { + list-style: none; + padding: 0; + margin: 0; + display: flex; + gap: 0; +} + +.tab-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 24px; + background: transparent; + border: none; + border-bottom: 3px solid transparent; + border-radius: 0; + font-size: 14px; + font-weight: 500; + color: var(--text-soft); + cursor: pointer; + transition: var(--transition); + font-family: "Roboto", Arial, sans-serif; + margin-bottom: -2px; + position: relative; +} + +.tab-btn:hover { + color: var(--navy); + background: transparent; +} + +.tab-btn.active { + background: transparent; + color: var(--navy); + border-bottom-color: var(--navy); + font-weight: 700; +} + +.tab-num { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + background: #e2e8f0; + color: var(--text-soft); + border-radius: 50%; + font-size: 12px; + font-weight: 700; + transition: var(--transition); +} + +.tab-btn.active .tab-num { + background: var(--navy); + color: #fff; +} + +.tab-panel { + display: none; +} + +.tab-panel.active { + display: block; +} + +.sop-card { + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.sop-card-header-blue { + background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%); + padding: 32px; + color: #fff; +} + +.sop-header-meta { + margin-bottom: 12px; +} + +.sop-code-badge { + display: inline-block; + background: rgba(254, 180, 1, .2); + color: var(--gold); + padding: 6px 14px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; +} + +.sop-header-title { + font-size: clamp(1.5rem, 2.5vw, 2rem); + font-weight: 700; + margin-bottom: 10px; + line-height: 1.2; +} + +.sop-header-desc { + font-size: 15px; + line-height: 1.6; + color: rgba(255, 255, 255, .9); +} + +.sop-body { + padding: 32px; +} + +.sop-block { + margin-bottom: 24px; +} + +.sop-block-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--navy); + margin-bottom: 10px; + padding-bottom: 6px; + border-bottom: 2px solid var(--line); +} + +.sop-block-text { + font-size: 14px; + color: var(--text-main); + line-height: 1.7; +} + +.sop-two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + margin-bottom: 24px; +} + +.sop-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.sop-list.bullets li { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; + color: var(--text-main); + line-height: 1.6; +} + +.sop-list.bullets li::before { + content: ''; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--navy); + flex-shrink: 0; + margin-top: 6px; +} + +.sop-list.numbered { + counter-reset: s; +} + +.sop-list.numbered li { + counter-increment: s; + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; + color: var(--text-main); + line-height: 1.6; +} + +.sop-list.numbered li::before { + content: counter(s); + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; + background: var(--navy); + color: #fff; + border-radius: 50%; + font-size: 11px; + font-weight: 700; + flex-shrink: 0; + margin-top: 2px; +} + +.sop-staff-block { + margin-bottom: 24px; +} + +.sop-staff-pills { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px; +} + +.sop-staff-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--navy); + color: #fff; + padding: 8px 14px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; +} + +.sop-info-box { + display: flex; + align-items: flex-start; + gap: 12px; + background: var(--gold-pale); + border: 1px solid #e0c06a; + border-radius: var(--radius-sm); + padding: 16px 20px; + margin-bottom: 24px; +} + +.sop-info-box .iico { + font-size: 18px; + flex-shrink: 0; + margin-top: 2px; +} + +.sop-info-box-text { + font-size: 14px; + color: #5a4000; + line-height: 1.7; +} + +.sop-info-box-text strong { + color: #3a2800; +} + +.sop-card-footer { + padding: 18px 32px; + display: flex; + align-items: center; + gap: 12px; + border-top: 1px solid var(--line); + flex-wrap: wrap; +} + +.btn-preview { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--navy); + color: #fff !important; + padding: 11px 20px; + border-radius: var(--radius-sm); + font-size: 14px; + font-weight: 700; + border: none; + cursor: pointer; + transition: var(--transition); + text-decoration: none; +} + +.btn-preview:hover { + background: var(--navy-light); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.is-hidden { + display: none !important; +} + +/* EMPTY STATE MESSAGE */ +.empty-state-msg { + text-align: center; + padding: 40px 20px; + color: var(--text-soft); + font-size: 15px; + font-style: italic; + border: 1px dashed var(--line); + border-radius: var(--radius); + background: var(--bg-soft); + margin: 0; +} + +/* FAQ SECTION */ +#assignment-body .faq-section-wrap { + background: transparent; + padding: 60px 0; + border-radius: 12px; + margin: 60px 0; +} + +#assignment-body .faq-section-inner { + margin: 0; +} + +#assignment-body .faq-header { + margin-bottom: 36px; +} + +#assignment-body .faq-header h2 { + font-size: clamp(1.5rem, 2.5vw, 2rem); + font-weight: 700; + color: var(--navy); + line-height: 1.2; + margin: 8px 0 0; +} + +#assignment-body .faq-header p { + font-size: 15px; + color: var(--text-soft); + margin-top: 10px; + line-height: 1.6; +} + +#assignment-body .faq-wrap { + display: flex; + flex-direction: column; + gap: 12px; +} + +#assignment-body .faq-item { + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: var(--shadow-sm); + transition: box-shadow .25s ease, border-color .25s ease; +} + +#assignment-body .faq-item:hover { + box-shadow: var(--shadow-md); +} + +#assignment-body .faq-item.open { + border-color: rgba(0, 49, 80, .3); + box-shadow: var(--shadow-md); +} + +#assignment-body .faq-question { + padding: 20px 24px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + gap: 16px; + user-select: none; +} + +#assignment-body .faq-question-text { + font-size: 15px; + font-weight: 600; + color: var(--navy); + line-height: 1.5; +} + +#assignment-body .faq-icon { + width: 30px; + height: 30px; + border-radius: 50%; + background: var(--bg-soft); + border: 1.5px solid var(--line); + color: var(--navy); + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + flex-shrink: 0; + transition: transform .3s ease, background .3s ease, border-color .3s ease, color .3s ease; +} + +#assignment-body .faq-item.open .faq-icon { + transform: rotate(45deg); + background: var(--navy); + color: var(--gold); + border-color: var(--navy); +} + +#assignment-body .faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height .45s cubic-bezier(.4,0,.2,1); +} + +#assignment-body .faq-item.open .faq-answer { + max-height: 400px; +} + +#assignment-body .faq-answer-inner { + margin: 0 24px; + padding: 16px 0 20px; + font-size: 14px; + color: var(--text-main); + line-height: 1.8; + border-top: 1px solid var(--line); +} + +/* SCROLL REVEAL */ +.reveal { + opacity: 0; + transform: translateY(30px); + transition: opacity .65s ease, transform .65s ease; +} + +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +.reveal-delay-1 { + transition-delay: .1s; +} + +.reveal-delay-2 { + transition-delay: .2s; +} + +.reveal-delay-3 { + transition-delay: .3s; +} + +/* RESPONSIVE */ +@media (max-width: 767px) { + .staff-grid { + grid-template-columns: 1fr; + } + + .sop-two-col { + grid-template-columns: 1fr; + gap: 20px; + } + + .content-section, + .sop-section, + #assignment-body .faq-section-wrap { + padding: 40px 20px; + } +} + +@media (max-width: 480px) { + .tab-btn { + padding: 12px 16px; + font-size: 13px; + width: 100%; + } + + .sop-card-header-blue, + .sop-body, + .sop-card-footer { + padding: 20px; + } + + .staff-card-actions { + flex-direction: column; + } + + .btn-staff { + width: 100%; + } + + #assignment-body .faq-question { + padding: 16px 18px; + } + + #assignment-body .faq-answer-inner { + margin: 0 18px; + } +} + +/* ============================================================ */ +/* SOP REDESIGN: SEARCH + FILTER + CARD GRID + MODAL */ +/* ============================================================ */ + +/* --- Toolbar (search + chips) --- */ +.sop-toolbar { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 28px; + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px 24px; + box-shadow: var(--shadow-sm); +} + +@media (min-width: 768px) { + .sop-toolbar { + flex-direction: row; + align-items: center; + flex-wrap: wrap; + } +} + +/* --- Search Input --- */ +.sop-search-wrap { + position: relative; + flex: 1; + min-width: 240px; +} + +.sop-search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: var(--text-soft); + font-size: 14px; + pointer-events: none; +} + +.sop-search-input { + width: 100%; + padding: 11px 40px 11px 40px; + border: 1.5px solid var(--line); + border-radius: 8px; + font-size: 14px; + font-family: "Roboto", Arial, sans-serif; + color: var(--text-main); + background: var(--bg-soft); + outline: none; + transition: border-color var(--transition), box-shadow var(--transition); + -webkit-appearance: none; + appearance: none; +} + +.sop-search-input:focus { + border-color: var(--navy); + box-shadow: 0 0 0 3px rgba(0, 49, 80, 0.1); + background: #fff; +} + +.sop-search-input::placeholder { + color: #94a3b8; +} + +/* Remove default browser X in search input */ +.sop-search-input::-webkit-search-cancel-button { display: none; } + +.sop-search-clear { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: #e2e8f0; + border: none; + border-radius: 50%; + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 10px; + color: #64748b; + transition: background var(--transition); +} + +.sop-search-clear:hover { + background: #cbd5e1; +} + +/* --- Filter Chips --- */ +.sop-filter-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sop-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 20px; + border: 1.5px solid var(--line); + background: var(--bg-soft); + color: var(--text-soft); + font-size: 13px; + font-weight: 600; + font-family: "Roboto", Arial, sans-serif; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.sop-chip:hover { + border-color: var(--navy); + color: var(--navy); + background: #f0f4f8; +} + +.sop-chip.active { + background: var(--navy); + color: #fff; + border-color: var(--navy); +} + +.sop-chip.active:hover { + background: var(--navy-light); +} + +/* --- Result Info --- */ +.sop-result-info { + font-size: 13px; + color: var(--text-soft); + margin-bottom: 20px; + font-weight: 500; + padding-left: 2px; +} + +/* --- SOP Card Grid --- */ +.sop-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 24px; +} + +/* --- SOP Grid Card --- */ +.sop-grid-card { + background: #fff; + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 0; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: var(--shadow-sm); + transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition); + position: relative; +} + +.sop-grid-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--navy), var(--gold)); + opacity: 0; + transition: opacity var(--transition); +} + +.sop-grid-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-md); + border-color: rgba(0, 49, 80, 0.2); +} + +.sop-grid-card:hover::before { + opacity: 1; +} + +/* Card header row (icon + title + category) */ +.sgc-header { + display: flex; + align-items: center; + gap: 14px; + padding: 20px 20px 14px; +} + +.sgc-icon-wrap { + width: 46px; + height: 46px; + border-radius: 10px; + background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; + flex-shrink: 0; +} + +.sgc-meta { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; +} + +.sgc-code { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-soft); +} + +/* Category badge */ +.sgc-category { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 3px 10px; + border-radius: 20px; + width: fit-content; +} + +.sop-cat-akademik { + background: rgba(59, 130, 246, 0.12); + color: #1d4ed8; +} + +.sop-cat-praktikum { + background: rgba(16, 185, 129, 0.12); + color: #047857; +} + +/* Category row (between header and description) */ +.sgc-category-row { + padding: 0 20px; + margin-bottom: 10px; +} + +/* Card title */ +.sgc-title { + font-size: 14px; + font-weight: 700; + color: var(--navy); + line-height: 1.35; + margin: 0; + white-space: normal; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* Card description */ +.sgc-desc { + font-size: 13px; + color: var(--text-soft); + line-height: 1.6; + margin: 0 20px 0; + flex: 1; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Card footer */ +.sgc-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px 18px; + margin-top: 16px; + border-top: 1px solid var(--line); + gap: 8px; +} + +.sgc-steps { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-soft); + font-weight: 500; +} + +.sgc-steps i { + color: var(--gold); +} + +/* Detail button on card */ +.sgc-detail-btn { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--navy); + color: #fff; + border: none; + border-radius: 6px; + padding: 8px 14px; + font-size: 12px; + font-weight: 700; + font-family: "Roboto", Arial, sans-serif; + cursor: pointer; + transition: background var(--transition), transform var(--transition); + white-space: nowrap; +} + +.sgc-detail-btn:hover { + background: var(--navy-light); + transform: translateX(2px); +} + +.sgc-detail-btn i { + font-size: 10px; + transition: transform var(--transition); +} + +.sgc-detail-btn:hover i { + transform: translateX(3px); +} + +/* --- Empty State --- */ +.sop-empty-state { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; +} + +.sop-empty-icon { + font-size: 48px; + color: #cbd5e1; + margin-bottom: 16px; +} + +.sop-empty-title { + font-size: 18px; + font-weight: 700; + color: var(--text-main); + margin: 0 0 8px; +} + +.sop-empty-desc { + font-size: 14px; + color: var(--text-soft); + margin: 0 0 20px; +} + +.sop-empty-reset { + display: inline-flex; + align-items: center; + gap: 8px; + background: var(--navy); + color: #fff; + border: none; + border-radius: 8px; + padding: 10px 20px; + font-size: 14px; + font-weight: 600; + font-family: "Roboto", Arial, sans-serif; + cursor: pointer; + transition: background var(--transition); +} + +.sop-empty-reset:hover { + background: var(--navy-light); +} + +/* --- SOP Detail Modal --- */ +.sop-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + opacity: 0; + pointer-events: none; + transition: opacity 0.18s ease; +} + +.sop-modal-overlay.active { + opacity: 1; + pointer-events: auto; +} + +.sop-modal-box { + background: #fff; + border-radius: 16px; + max-width: 780px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + box-shadow: 0 32px 80px rgba(0, 0, 0, 0.25); + position: relative; + transform: scale(0.96) translateY(10px); + transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.sop-modal-overlay.active .sop-modal-box { + transform: scale(1) translateY(0); +} + +.sop-modal-close { + position: sticky; + top: 16px; + float: right; + right: 16px; + width: 34px; + height: 34px; + border-radius: 50%; + border: none; + background: rgba(0, 0, 0, 0.08); + color: #475569; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + margin: 16px 16px 0 0; + z-index: 10; + transition: background var(--transition), color var(--transition); +} + +.sop-modal-close:hover { + background: var(--navy); + color: #fff; +} + +/* Modal header */ +.sop-modal-header { + background: linear-gradient(135deg, var(--navy) 0%, var(--navy-light) 100%); + padding: 28px 32px 28px; + color: #fff; + border-radius: 16px 16px 0 0; +} + +.sop-modal-header-top { + display: flex; + align-items: flex-start; + gap: 20px; +} + +.sop-modal-icon-wrap { + width: 56px; + height: 56px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + flex-shrink: 0; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.sop-modal-badges { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.sop-modal-badges .sgc-category { + background: rgba(255, 255, 255, 0.2); + color: #fff; +} + +.sop-modal-title { + font-size: clamp(1.2rem, 2.5vw, 1.6rem); + font-weight: 700; + color: #fff; + line-height: 1.2; + margin: 0 0 8px; +} + +.sop-modal-desc { + font-size: 14px; + color: rgba(255, 255, 255, 0.85); + line-height: 1.6; + margin: 0; +} + +/* Modal body */ +.sop-modal-body { + padding: 28px 32px 32px; +} + +/* Responsive modal */ +@media (max-width: 600px) { + .sop-modal-header { + padding: 20px; + } + + .sop-modal-header-top { + flex-direction: column; + } + + .sop-modal-body { + padding: 20px; + } + + .sop-card-grid { + grid-template-columns: 1fr; + } + + .sop-toolbar { + padding: 16px; + } +}