forked from izu/student-web-if-development-kit
feat(C4A): implementasi hero section, staf section, sop section, faq section
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 [];
|
||||
}
|
||||
}
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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);
|
||||
@@ -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<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">Data staf pendukung tidak tersedia saat ini.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
staffs.forEach((staff, index) => {
|
||||
const {
|
||||
name,
|
||||
role,
|
||||
image,
|
||||
location,
|
||||
phone,
|
||||
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 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">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">Jam Layanan</span>
|
||||
<div class="schedule-list">${scheduleHTML}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="staff-info-item"><span class="icon">📞</span><div><span class="label">Kontak</span><span class="val">${phone}</span></div></div>
|
||||
</div>
|
||||
<div class="staff-divider"></div>
|
||||
<div class="staff-tasks-title">Tugas & Tanggung Jawab</div>
|
||||
<ul class="staff-tasks">${tasksHTML}</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 `
|
||||
<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} Langkah</span>
|
||||
</div>
|
||||
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="Lihat Detail ${title}">
|
||||
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">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>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">Tujuan</div>
|
||||
<p class="sop-block-text">${purpose}</p>
|
||||
</div>
|
||||
<div class="sop-two-col">
|
||||
<div>
|
||||
<div class="sop-block-label">Persyaratan</div>
|
||||
<ul class="sop-list bullets">${reqsHTML}</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sop-block-label">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">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) {
|
||||
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<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">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;
|
||||
}
|
||||
Reference in New Issue
Block a user