14 Commits

Author SHA1 Message Date
cygouw 0c9ece813a chore(localization): merge upstream updates and preserve C4A keys 2026-06-05 11:10:05 +07:00
cygouw d765783877 fix(C4A): resolve index.html conflict 2026-06-05 10:41:27 +07:00
cygouw f1cd7a8f5f merge branch 'main' into feature/bilingual-support 2026-06-05 10:33:20 +07:00
cygouw ce3c2e6ae8 fix(C4A): resolve merge conflict 2026-06-05 05:24:40 +02:00
cygouw 6430aee79e fix(C4A): resolve merge conflict on index.html - fix h1 title and remove BOM 2026-06-05 05:17:00 +02:00
cygouw 2fc6b7b6bc fix(C4A): resolve conflict - fix h1 title in index.html 2026-06-05 05:11:07 +02:00
cygouw d6db9cc464 chore(localization): sync translation keys with upstream main 2026-06-05 09:33:14 +07:00
cygouw ab4493e589 fix: resolve merge conflict from feature/bilingual-support 2026-06-05 09:10:14 +07:00
cygouw 154550d3b4 Merge branch 'feature/bilingual-support' 2026-06-05 08:33:51 +07:00
AufaHafizah 3975798ba2 Merge pull request 'feature/wa-link-protection' (#2) from feature/wa-link-protection into main
Reviewed-on: #2
2026-06-05 00:50:40 +00:00
AufaHafizah b943518500 Merge pull request 'feature/halaman-stafPendukung-SOP' (#1) from feature/halaman-stafPendukung-SOP into main
Reviewed-on: #1
2026-06-05 00:48:19 +00:00
cygouw 3ead384ec9 chore(c4a): update assignment page title 2026-06-05 01:46:57 +07:00
cygouw 5ccdba561e fix(ui): re-init animations after language toggle to prevent content from disappearing 2026-06-04 23:40:06 +07:00
cygouw 2edbcb0c88 feat(ui): add language switcher for Indonesian and English 2026-06-04 23:31:09 +07:00
5 changed files with 794 additions and 162 deletions
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1150,7 +1150,8 @@
<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 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> Halaman Statis</span>
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Judul Halaman Group 04</h1> <h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Staf Pendukung dan Standar Operasional
Prosedur (SOP)</h1>
<p class="text-slate-500 text-sm">Kelompok 04 &mdash; C4A &nbsp;&middot;&nbsp; Informatika UNTAN 2026 <p class="text-slate-500 text-sm">Kelompok 04 &mdash; C4A &nbsp;&middot;&nbsp; Informatika UNTAN 2026
</p> </p>
</div> </div>
@@ -1162,7 +1163,7 @@
<div class="c4a-hero-tag">LAYANAN AKADEMIK &bull; INFORMATIKA UNTAN <div class="c4a-hero-tag">LAYANAN AKADEMIK &bull; INFORMATIKA UNTAN
</div> </div>
<h1 class="c4a-hero-title"> <h1 class="c4a-hero-title">
Staf Pendukung &amp; <br/> Staf Pendukung &amp; <br />
<span class="highlight-orange">Standar Operasional Prosedur (SOP)</span> <span class="highlight-orange">Standar Operasional Prosedur (SOP)</span>
</h1> </h1>
<p class="c4a-hero-desc">Halaman ini menampilkan informasi petugas <p class="c4a-hero-desc">Halaman ini menampilkan informasi petugas
+2
View File
@@ -52,3 +52,5 @@ export async function getFaqData() {
return []; return [];
} }
} }
+20 -5
View File
@@ -10,12 +10,14 @@ import {
renderFaqSection, renderFaqSection,
initSopInteraction, initSopInteraction,
initAnimations, initAnimations,
initFaq initFaq,
applySopFilter
} from './ui.js'; } from './ui.js';
// Cache data secara global untuk mendukung rendering UI // Cache data secara global untuk mendukung rendering UI
let cachedStaffData = []; let cachedStaffData = [];
let cachedSopData = []; let cachedSopData = [];
let cachedFaqData = [];
/** /**
* Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel, * Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel,
@@ -32,10 +34,11 @@ async function initApp() {
cachedStaffData = staffData || []; cachedStaffData = staffData || [];
cachedSopData = sopData || []; cachedSopData = sopData || [];
cachedFaqData = faqData || [];
renderStaffCards(cachedStaffData); renderStaffCards(cachedStaffData);
renderSopCards(cachedSopData); renderSopCards(cachedSopData);
renderFaqSection(faqData || []); renderFaqSection(cachedFaqData);
initSopInteraction(); initSopInteraction();
initFaq(); initFaq();
@@ -45,12 +48,24 @@ async function initApp() {
initAnimations(); initAnimations();
}, 100); }, 100);
} catch (error) { } catch (error) {
console.error('Terjadi kesalahan saat menginisialisasi aplikasi C4A:', error); console.error('Terjadi kesalahan saat menginisialisasi halaman:', error);
} }
} }
// NOTE: Fitur bilingual sudah dihapus - languageChanged listener tidak lagi diperlukan // Re-render UI saat bahasa berubah (data tetap sama, hanya label UI yang berganti via t())
// Silakan hapus atau archive jika ada implementasi global untuk bahasa 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 // Jalankan inisialisasi aplikasi saat DOM siap
document.addEventListener('DOMContentLoaded', initApp); document.addEventListener('DOMContentLoaded', initApp);
+43 -23
View File
@@ -1,5 +1,17 @@
// --- Helper Fungsi --- // --- 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... * Normalisasi nomor telepon ke format standard e.g. 628...
* @param {string} phone - Nomor telepon mentah * @param {string} phone - Nomor telepon mentah
@@ -70,7 +82,7 @@ export function renderStaffCards(staffs) {
container.innerHTML = ''; container.innerHTML = '';
if (!staffs || staffs.length === 0) { if (!staffs || staffs.length === 0) {
container.innerHTML = `<p class="empty-state-msg">Data staf pendukung tidak tersedia saat ini.</p>`; container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_staff', 'Data staf pendukung tidak tersedia saat ini.')}</p>`;
return; return;
} }
@@ -88,8 +100,8 @@ export function renderStaffCards(staffs) {
const scheduleHTML = schedule.map(sched => const scheduleHTML = schedule.map(sched =>
`<div class="schedule-item">` + `<div class="schedule-item">` +
`<span class="s-day">${sched.day}</span>` + `<span class="s-day">${sched.day}</span>` +
`<span class="s-time">${sched.time} <span class="s-break">| ${sched.break}</span></span>` + `<span class="s-time">${sched.time} <span class="s-break">| ${sched.break}</span></span>` +
`</div>` `</div>`
).join(''); ).join('');
@@ -117,23 +129,24 @@ export function renderStaffCards(staffs) {
</div> </div>
<div class="staff-card-body"> <div class="staff-card-body">
<div class="staff-info-grid"> <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><span class="label">${t('grp_c4a_label_location', 'Lokasi')}</span><span class="val">${location}</span></div></div>
<div class="staff-info-item"> <div class="staff-info-item">
<span class="icon">🕒</span> <span class="icon">🕒</span>
<div style="flex:1"> <div style="flex:1">
<span class="label">Jam Layanan</span> <span class="label">${t('grp_c4a_label_service_hours', 'Jam Layanan')}</span>
<div class="schedule-list">${scheduleHTML}</div> <div class="schedule-list">${scheduleHTML}</div>
</div> </div>
</div> </div>
<div class="staff-info-item staff-contact-item ${hideContactItemClass}"><span class="icon">📞</span><div><span class="label">Kontak WhatsApp</span><span class="val">${displayPhone}</span></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>
<div class="staff-divider"></div> <div class="staff-divider"></div>
<div class="staff-tasks-title">Tugas & Tanggung Jawab</div> <div class="staff-tasks-title">${t('grp_c4a_label_tasks', 'Tugas & Tanggung Jawab')}</div>
<ul class="staff-tasks">${tasksHTML}</ul> <ul class="staff-tasks">${tasksHTML}</ul>
</div> </div>
<div class="staff-card-actions staff-contact-actions ${hideContactActionClass}"> <div class="staff-card-actions staff-contact-actions ${hideContactActionClass}">
<button class="btn-staff btn-staff-wa" data-wa="${encodedPhone}" aria-label="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> Kirim Pesan WA</button> <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> </div>
`; `;
container.appendChild(card); container.appendChild(card);
@@ -165,8 +178,11 @@ const CATEGORY_COLORS = {
* @returns {string} Label kategori * @returns {string} Label kategori
*/ */
function getCatLabel(category) { function getCatLabel(category) {
const map = { akademik: 'Akademik', praktikum: 'Praktikum' }; const map = {
return map[category] || 'Akademik'; 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');
} }
/** /**
@@ -204,10 +220,10 @@ function buildSopCard(sop) {
<div class="sgc-footer"> <div class="sgc-footer">
<div class="sgc-steps"> <div class="sgc-steps">
<i class="fas fa-list-ol"></i> <i class="fas fa-list-ol"></i>
<span>${procedures.length} Langkah</span> <span>${procedures.length} ${t('grp_c4a_label_steps', 'Langkah')}</span>
</div> </div>
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="Lihat Detail ${title}"> <button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="${t('grp_c4a_label_view_detail', 'Lihat Detail')} ${title}">
Lihat Detail <i class="fas fa-arrow-right"></i> ${t('grp_c4a_label_view_detail', 'Lihat Detail')} <i class="fas fa-arrow-right"></i>
</button> </button>
</div> </div>
</article>`; </article>`;
@@ -244,7 +260,7 @@ function buildSopModalContent(sop) {
: ''; : '';
const staffBlockHTML = hasStaff const staffBlockHTML = hasStaff
? `<div class="sop-staff-block"> ? `<div class="sop-staff-block">
<div class="sop-block-label">Staf Bertanggung Jawab (Bersifat Opsional)</div> <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 class="sop-staff-pills">${staffHTML}</div>
</div>` </div>`
: ''; : '';
@@ -253,7 +269,7 @@ function buildSopModalContent(sop) {
const additionalInfoHTML = additionalInfo const additionalInfoHTML = additionalInfo
? `<div class="sop-info-box"> ? `<div class="sop-info-box">
<span class="iico">️</span> <span class="iico">️</span>
<p class="sop-info-box-text"><strong>Informasi Tambahan (Bersifat Opsional):</strong> ${additionalInfo}</p> <p class="sop-info-box-text"><strong>${t('grp_c4a_label_additional_info', 'Informasi Tambahan (Bersifat Opsional):')}</strong> ${additionalInfo}</p>
</div>` </div>`
: ''; : '';
@@ -274,16 +290,16 @@ function buildSopModalContent(sop) {
</div> </div>
<div class="sop-modal-body"> <div class="sop-modal-body">
<div class="sop-block"> <div class="sop-block">
<div class="sop-block-label">Tujuan</div> <div class="sop-block-label">${t('grp_c4a_label_purpose', 'Tujuan')}</div>
<p class="sop-block-text">${purpose}</p> <p class="sop-block-text">${purpose}</p>
</div> </div>
<div class="sop-two-col"> <div class="sop-two-col">
<div> <div>
<div class="sop-block-label">Persyaratan</div> <div class="sop-block-label">${t('grp_c4a_label_requirements', 'Persyaratan')}</div>
<ul class="sop-list bullets">${reqsHTML}</ul> <ul class="sop-list bullets">${reqsHTML}</ul>
</div> </div>
<div> <div>
<div class="sop-block-label">Alur Prosedur</div> <div class="sop-block-label">${t('grp_c4a_label_procedure', 'Alur Prosedur')}</div>
<ul class="sop-list numbered">${procsHTML}</ul> <ul class="sop-list numbered">${procsHTML}</ul>
</div> </div>
</div> </div>
@@ -305,7 +321,7 @@ export function renderSopCards(sops) {
// Tampilkan pesan jika data SOP kosong atau gagal dimuat // Tampilkan pesan jika data SOP kosong atau gagal dimuat
if (!sops || sops.length === 0) { if (!sops || sops.length === 0) {
container.innerHTML = `<p class="empty-state-msg">Data SOP tidak tersedia saat ini.</p>`; container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_sop', 'Data SOP tidak tersedia saat ini.')}</p>`;
updateResultInfo(0, 0); updateResultInfo(0, 0);
return; return;
} }
@@ -319,16 +335,18 @@ function updateResultInfo(shown, total) {
const el = document.getElementById('sop-result-info'); const el = document.getElementById('sop-result-info');
if (!el) return; if (!el) return;
if (shown === total) { if (shown === total) {
el.textContent = `Menampilkan ${total} SOP`; const allTemplate = t('grp_c4a_sop_showing_all', 'Menampilkan {total} SOP');
el.textContent = allTemplate.replace('{total}', total);
} else { } else {
el.textContent = `Menampilkan ${shown} dari ${total} SOP`; 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. * Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif.
*/ */
function applySopFilter() { export function applySopFilter() {
const container = document.getElementById('sop-cards-container'); const container = document.getElementById('sop-cards-container');
if (!container) return; if (!container) return;
@@ -358,6 +376,7 @@ function applySopFilter() {
updateResultInfo(visibleCount, cards.length); updateResultInfo(visibleCount, cards.length);
} }
/** /**
* Membuka modal detail untuk SOP tertentu berdasarkan tab_id. * Membuka modal detail untuk SOP tertentu berdasarkan tab_id.
* @param {string} tabId - ID unik tab/SOP yang ingin ditampilkan * @param {string} tabId - ID unik tab/SOP yang ingin ditampilkan
@@ -595,10 +614,11 @@ export function renderFaqSection(faqs) {
if (!container) return; if (!container) return;
if (!faqs || faqs.length === 0) { if (!faqs || faqs.length === 0) {
container.innerHTML = `<p class="empty-state-msg">Data FAQ tidak tersedia saat ini.</p>`; container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_faq', 'Data FAQ tidak tersedia saat ini.')}</p>`;
return; return;
} }
const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3']; const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3'];
const itemsHTML = faqs.map((faq, index) => { const itemsHTML = faqs.map((faq, index) => {