Update bilingual scholarship content

This commit is contained in:
2026-06-05 09:10:55 +07:00
parent a40a67875c
commit e2e36f1b92
9 changed files with 384 additions and 336 deletions
+70 -39
View File
@@ -1,53 +1,86 @@
// =====================================================
// carousel.js — Swiper.js Carousel Testimoni
// Touch-friendly, auto-play, responsive breakpoints
// + DUKUNGAN MULTI-BAHASA (ID/EN) Terintegrasi
// =====================================================
let testimoniSwiper = null;
let allTestimoniData = []; // Simpan data di memori agar tidak perlu loading ulang saat ganti bahasa
// 1. Variabel penampung bahasa yang terhubung otomatis dengan tombol ID/EN
let activeCarouselLang = localStorage.getItem('if_untan_lang') || document.documentElement.lang || 'id';
// 2. Tangkap event perubahan bahasa secara langsung dari localization.js
document.addEventListener('languageChanged', (e) => {
// Update bahasa
if (e.detail && e.detail.lang) {
activeCarouselLang = e.detail.lang;
} else if (typeof window.getLang === 'function') {
activeCarouselLang = window.getLang();
} else {
activeCarouselLang = document.documentElement.lang || 'id';
}
// Jika data sudah ada, langsung cetak ulang testimoninya
if (allTestimoniData && allTestimoniData.length > 0) {
renderTestimoniSlides(allTestimoniData);
initSwiper(allTestimoniData.length); // Swiper harus di-restart agar animasinya tidak rusak
}
});
/** Render slide testimoni ke dalam swiper-wrapper */
function renderTestimoniSlides(data) {
const wrapper = document.getElementById('testimonial-wrapper');
if (!wrapper) return;
const lang = activeCarouselLang; // Gunakan bahasa terbaru
wrapper.innerHTML = data.map(t => `
<div class="swiper-slide" style="height:auto;">
<div class="testimonial-card">
<img
src="${t.foto}"
alt="Foto ${t.nama}"
class="testimonial-avatar"
loading="lazy"
onerror="
this.style.display='none';
this.nextElementSibling.style.display='flex';
"
>
<div
class="testimonial-avatar"
style="
display:none;
background:linear-gradient(135deg,#003150,#4a6bc5);
align-items:center;
justify-content:center;
color:white;
font-size:22px;
font-weight:700;
"
>
${t.nama.charAt(0).toUpperCase()}
wrapper.innerHTML = data.map(t => {
// Logika Pintar: Cek mode bahasa. Jika 'en' dan teks terjemahannya ada, pakai bahasa Inggris.
// Jika tidak, tetap tampilkan teks bahasa Indonesia (sebagai cadangan).
const beasiswaName = (lang === 'en' && t.nama_beasiswa_en) ? t.nama_beasiswa_en : t.nama_beasiswa;
const quote = (lang === 'en' && t.kutipan_en) ? t.kutipan_en : t.kutipan;
return `
<div class="swiper-slide" style="height:auto;">
<div class="testimonial-card">
<img
src="${t.foto}"
alt="Foto ${t.nama}"
class="testimonial-avatar"
loading="lazy"
onerror="
this.style.display='none';
this.nextElementSibling.style.display='flex';
"
>
<div
class="testimonial-avatar"
style="
display:none;
background:linear-gradient(135deg,#003150,#4a6bc5);
align-items:center;
justify-content:center;
color:white;
font-size:22px;
font-weight:700;
"
>
${t.nama.charAt(0).toUpperCase()}
</div>
<div class="testimonial-name">${t.nama}</div>
<div class="testimonial-beasiswa">${beasiswaName}</div>
<p class="testimonial-quote">"${quote}"</p>
</div>
<div class="testimonial-name">${t.nama}</div>
<div class="testimonial-beasiswa">${t.nama_beasiswa}</div>
<p class="testimonial-quote">"${t.kutipan}"</p>
</div>
</div>
`).join('');
`;
}).join('');
}
/** Inisialisasi Swiper setelah slide di-render */
function initSwiper(count) {
// Bersihkan swiper lama jika ada (penting agar tidak menumpuk saat ganti bahasa)
if (testimoniSwiper) {
testimoniSwiper.destroy(true, true);
testimoniSwiper = null;
@@ -72,9 +105,10 @@ function initSwiper(count) {
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
},
// Dukungan Aksesibilitas Dinamis (VoiceOver / Screen Reader)
a11y: {
prevSlideMessage: 'Slide sebelumnya',
nextSlideMessage: 'Slide berikutnya',
prevSlideMessage: activeCarouselLang === 'en' ? 'Previous slide' : 'Slide sebelumnya',
nextSlideMessage: activeCarouselLang === 'en' ? 'Next slide' : 'Slide berikutnya',
},
});
}
@@ -88,22 +122,19 @@ async function initCarousel() {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
allTestimoniData = data; // Simpan ke variabel global
if (!data || data.length === 0) {
// Sembunyikan seksi testimoni jika data kosong (sesuai UC-07 Alt Flow)
section?.classList.add('hidden');
return;
}
renderTestimoniSlides(data);
// Init Swiper setelah DOM di-render
requestAnimationFrame(() => initSwiper(data.length));
} catch {
// Jika gagal fetch, sembunyikan seksi (graceful degradation)
section?.classList.add('hidden');
}
}
document.addEventListener('DOMContentLoaded', initCarousel);
document.addEventListener('DOMContentLoaded', initCarousel);
+103 -85
View File
@@ -1,70 +1,113 @@
// =====================================================
// filter.js — Fetch JSON, Render Kartu, Filter Tab, Pencarian, Paginasi
// Filter dan search berjalan KUMULATIF (sesuai PROJECT_CONTEXT rule #6)
// Filter dan search berjalan KUMULATIF
// + DUKUNGAN MULTI-BAHASA (ID/EN) Terintegrasi
// =====================================================
const ITEMS_PER_PAGE = 6; // 2 baris × 3 kolom desktop
const ITEMS_PER_PAGE = 6;
let allData = []; // Semua data beasiswa dari JSON
let allData = [];
let activeFilter = 'semua';
let searchKeyword = '';
let currentPage = 1;
// Format tanggal
// 1. Variabel penampung bahasa yang terhubung otomatis dengan tombol ID/EN
let activeLang = localStorage.getItem('if_untan_lang') || document.documentElement.lang || 'id';
// 2. Tangkap event perubahan bahasa secara langsung dari localization.js
document.addEventListener('languageChanged', (e) => {
if (e.detail && e.detail.lang) {
activeLang = e.detail.lang; // Perbarui bahasa!
} else {
activeLang = document.documentElement.lang || 'id';
}
// Jika data sudah ada, langsung cetak ulang kartunya
if (allData.length > 0) {
renderCards();
}
});
// 3. Format tanggal cerdas berdasarkan bahasa
function formatDate(str) {
if (!str) return '—';
const d = new Date(str);
if (isNaN(d.getTime())) return str;
return d.toLocaleDateString('id-ID', {
return d.toLocaleDateString(activeLang === 'en' ? 'en-US' : 'id-ID', {
day: 'numeric', month: 'long', year: 'numeric'
});
}
// Sanitasi string untuk mencegah XSS pada search
// 4. Kamus mini untuk teks dinamis dari JSON (Negara & Kategori)
function translateDynamic(text, lang) {
if (!text || lang === 'id') return text;
const dict = {
'Amerika Serikat': 'United States',
'Swiss': 'Switzerland',
'Pemerintah': 'Government',
'Swasta': 'Private',
};
return dict[text] || text;
}
function sanitize(str) {
return str.replace(/[<>&"']/g, c =>
({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' }[c])
);
}
/**
* Buat elemen kartu beasiswa
* Gunakan textContent/setAttribute — hindari langsung innerHTML dari user input
*/
function createCard(bsw) {
// Tentukan badge & warna tombol status
const lang = activeLang; // Selalu gunakan bahasa aktif terbaru
// Terjemahan Tombol Status
let statusClass = 'status-btn-ditutup';
let statusLabel = 'Ditutup';
let statusLabel = lang === 'en' ? 'Closed' : 'Ditutup';
const statusLower = (bsw.status || '').toLowerCase();
if (statusLower === 'buka' || statusLower === 'dibuka') {
statusClass = 'status-btn-buka';
statusLabel = 'Pendaftaran Dibuka';
statusLabel = lang === 'en' ? 'Registration Open' : 'Pendaftaran Dibuka';
} else if (statusLower.includes('segera')) {
statusClass = 'status-btn-segera';
statusLabel = 'Segera Dibuka';
statusLabel = lang === 'en' ? 'Opening Soon' : 'Segera Dibuka';
}
// Ambil pills dari tags lokal: ["jenjang", "kategori/swasta/pemerintah", "negara", "tipe_pendanaan", ...]
// --- KAMUS MINI UNTUK KARTU ---
const textMulai = lang === 'en' ? 'Start' : 'Mulai';
const textDeadline = lang === 'en' ? 'Deadline' : 'Tenggat';
const textSelengkapnya = lang === 'en' ? 'Read More' : 'Selengkapnya';
// Ambil pills (tags) dan terjemahkan negaranya
const tags = Array.isArray(bsw.tags) ? bsw.tags : [];
const jenjang = tags[0] || '';
// Negara: cari tag yang bukan jenjang (S1/S2/S3), bukan kategori sumber (Swasta/Pemerintah),
// dan bukan tipe pendanaan (Fully/Partially Funded)
const skipPatterns = /^(S1|S2|S3|D3|D4|Swasta|Pemerintah|Fully Funded|Partially Funded|Beasiswa Penuh|Beasiswa Sebagian|Prestasi)$/i;
const negara = tags.slice(1).find(t => !skipPatterns.test(t)) || '';
const rawNegara = tags.slice(1).find(t => !skipPatterns.test(t)) || '';
const negara = translateDynamic(rawNegara, lang);
// Tipe pendanaan untuk subtitle — ambil dari kategori field atau dari deskripsi_singkat
// Terjemahan Subtitle (Kategori Beasiswa)
const tipe = bsw.kategori || '';
const rawTipe = tipe.replace(/^beasiswa\s+/i, '');
let subtitle = '';
if (rawTipe) {
if (lang === 'en') {
const tipeEn = translateDynamic(rawTipe, lang);
subtitle = `${tipeEn} Scholarship`;
} else {
subtitle = `Beasiswa ${rawTipe}`;
}
}
// Ambil nama dari versi Inggris jika ada
const namaBsw = (lang === 'en' && bsw.nama_en) ? bsw.nama_en : bsw.nama;
const pillsHtml = [jenjang, negara]
.filter(Boolean)
.map((t, i) => `<span class="card-pill ${i === 0 ? 'card-pill-level' : ''}">${t}</span>`)
.join('');
// Subtitle: hindari duplikasi "Beasiswa Beasiswa ..."
const rawTipe = tipe.replace(/^beasiswa\s+/i, '');
const subtitle = rawTipe ? `Beasiswa ${rawTipe}` : '';
const article = document.createElement('article');
article.className = 'scholarship-card fade-in-up';
article.setAttribute('role', 'listitem');
@@ -72,17 +115,17 @@ function createCard(bsw) {
article.innerHTML = `
<div class="card-body">
<h3 class="card-title">${bsw.nama}</h3>
<h3 class="card-title">${namaBsw}</h3>
${subtitle ? `<p class="card-subtitle">${subtitle}</p>` : ''}
${pillsHtml ? `<div class="card-pills">${pillsHtml}</div>` : ''}
<hr class="card-divider">
<div class="card-dates">
<div class="card-date-col">
<span class="card-date-label">Mulai</span>
<span class="card-date-label">${textMulai}</span>
<span class="card-date-value">${formatDate(bsw.tanggal_buka)}</span>
</div>
<div class="card-date-col">
<span class="card-date-label">Deadline</span>
<span class="card-date-label">${textDeadline}</span>
<span class="card-date-value">${formatDate(bsw.tanggal_tutup)}</span>
</div>
</div>
@@ -90,70 +133,67 @@ function createCard(bsw) {
<button
class="btn-selengkapnya"
onclick="openPopup('${bsw.id}')"
aria-label="Lihat detail beasiswa ${bsw.nama}"
>Selengkapnya</button>
aria-label="Lihat detail beasiswa ${namaBsw}"
>${textSelengkapnya}</button>
</div>
`;
return article;
}
/**
* Filter data KUMULATIF: filter tab DAN search harus terpenuhi keduanya
*/
function getFiltered() {
const kw = searchKeyword.toLowerCase();
return allData.filter(bsw => {
// Cocok dengan filter tab
const matchFilter =
activeFilter === 'semua' ||
bsw.status === activeFilter ||
bsw.kategori === activeFilter;
// Logika Filter Tab S1 & Studi Lanjut
let matchFilter = false;
const tags = bsw.tags || [];
// Cocok dengan pencarian (nama atau deskripsi atau tags)
const matchSearch =
!kw ||
bsw.nama.toLowerCase().includes(kw) ||
bsw.deskripsi_singkat.toLowerCase().includes(kw) ||
(bsw.tags || []).some(t => t.toLowerCase().includes(kw));
if (activeFilter === 'semua') {
matchFilter = true;
} else if (activeFilter === 's1') {
matchFilter = tags.some(t => t.includes('S1'));
} else if (activeFilter === 'studi_lanjut') {
matchFilter = tags.some(t => t.includes('S2') || t.includes('S3'));
}
// Logika Pencarian Search Bar
const matchSearch = !kw ||
bsw.nama.toLowerCase().includes(kw) ||
bsw.deskripsi_singkat.toLowerCase().includes(kw) ||
tags.some(t => t.toLowerCase().includes(kw));
return matchFilter && matchSearch;
});
}
/** Render kartu + paginasi ke DOM */
function renderCards() {
const grid = document.getElementById('beasiswa-grid');
const pagCont = document.getElementById('pagination');
const filtered = getFiltered();
const lang = activeLang; // Gunakan bahasa terbaru
// Bersihkan grid
grid.innerHTML = '';
if (filtered.length === 0) {
const emptyTitle = lang === 'en' ? 'No scholarships found' : 'Tidak ada beasiswa ditemukan';
const emptyDesc = lang === 'en' ? 'Try adjusting your filters or search keywords' : 'Coba ubah filter atau kata kunci pencarian Anda';
grid.innerHTML = `
<div class="empty-state" style="grid-column:1/-1">
<i class="fas fa-search-minus"></i>
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">
Tidak ada beasiswa ditemukan
</p>
<p style="font-size:13px;color:#cbd5e1;">
Coba ubah filter atau kata kunci pencarian Anda
</p>
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">${emptyTitle}</p>
<p style="font-size:13px;color:#cbd5e1;">${emptyDesc}</p>
</div>`;
pagCont.innerHTML = '';
return;
}
// Hitung paginasi
const totalPages = Math.ceil(filtered.length / ITEMS_PER_PAGE);
if (currentPage > totalPages) currentPage = totalPages;
const start = (currentPage - 1) * ITEMS_PER_PAGE;
const items = filtered.slice(start, start + ITEMS_PER_PAGE);
// Render kartu dengan delay animasi berurutan
items.forEach((bsw, i) => {
const card = createCard(bsw);
card.style.animationDelay = `${i * 55}ms`;
@@ -163,7 +203,6 @@ function renderCards() {
renderPagination(totalPages, pagCont);
}
/** Render tombol-tombol paginasi */
function renderPagination(total, container) {
container.innerHTML = '';
if (total <= 1) return;
@@ -173,12 +212,10 @@ function renderPagination(total, container) {
btn.className = `page-btn${isActive ? ' active' : ''}`;
btn.innerHTML = content;
btn.disabled = disabled;
btn.setAttribute('aria-label', typeof page === 'number' ? `Halaman ${page}` : content);
if (!disabled) {
btn.addEventListener('click', () => {
currentPage = page;
renderCards();
// Scroll halus ke atas direktori
const dir = document.getElementById('directory');
if (dir) {
const top = dir.getBoundingClientRect().top + window.scrollY - 120;
@@ -189,19 +226,13 @@ function renderPagination(total, container) {
return btn;
};
// Prev
container.appendChild(make('<i class="fas fa-chevron-left" style="font-size:11px;"></i>', currentPage - 1, currentPage === 1));
// Halaman
for (let i = 1; i <= total; i++) {
container.appendChild(make(i, i, false, i === currentPage));
}
// Next
container.appendChild(make('<i class="fas fa-chevron-right" style="font-size:11px;"></i>', currentPage + 1, currentPage === total));
}
/** Inisialisasi tab filter */
function initFilterTabs() {
document.querySelectorAll('.filter-tab').forEach(btn => {
btn.addEventListener('click', () => {
@@ -218,47 +249,34 @@ function initFilterTabs() {
});
}
/** Inisialisasi real-time search */
function initSearch() {
const input = document.getElementById('search-input');
if (!input) return;
input.addEventListener('input', () => {
// Sanitasi input sebelum digunakan (cegah XSS)
searchKeyword = sanitize(input.value).trim();
currentPage = 1;
renderCards();
});
}
/** Fetch data via API.js (BeasiswaAPI) lalu render */
async function initDirectory() {
const grid = document.getElementById('beasiswa-grid');
try {
if (!window.BeasiswaAPI) {
throw new Error('Modul API.js (BeasiswaAPI) belum dimuat');
}
if (!window.BeasiswaAPI) throw new Error('Modul API.js (BeasiswaAPI) belum dimuat');
const { data, source } = await window.BeasiswaAPI.getBeasiswa();
allData = data;
console.info(`[items] beasiswa dimuat dari sumber: ${source} (${allData.length} item)`);
// Simpan ke window agar popup.js bisa akses
window.beasiswaData = allData;
renderCards();
} catch (err) {
console.error('[items] gagal memuat beasiswa', err);
const lang = activeLang;
const errTitle = lang === 'en' ? 'Failed to load scholarship data' : 'Gagal memuat data beasiswa';
const errDesc = lang === 'en' ? 'Could not connect to the server. Please refresh or contact admin.' : 'Tidak dapat terhubung ke server. Silakan refresh atau hubungi admin.';
grid.innerHTML = `
<div class="empty-state" style="grid-column:1/-1">
<i class="fas fa-exclamation-triangle" style="color:#fca5a5;"></i>
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">
Gagal memuat data beasiswa
</p>
<p style="font-size:13px;color:#cbd5e1;">
Tidak dapat terhubung ke server. Silakan refresh atau hubungi admin.
</p>
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">${errTitle}</p>
<p style="font-size:13px;color:#cbd5e1;">${errDesc}</p>
</div>`;
}
}
@@ -267,4 +285,4 @@ document.addEventListener('DOMContentLoaded', () => {
initFilterTabs();
initSearch();
initDirectory();
});
});
-136
View File
@@ -1,136 +0,0 @@
// =====================================================
// i18n.js — Toggle Bahasa Indonesia / English
// Sesuai PROJECT_CONTEXT: tersimpan di sessionStorage
// =====================================================
const i18nData = {
id: {
'nav.tentang': 'TENTANG KAMI',
'nav.program': 'PROGRAM',
'nav.sdm': 'SDM',
'nav.fasilitas': 'FASILITAS',
'nav.agenda': 'KARYA & AGENDA',
'nav.kontak': 'KONTAK',
'breadcrumb.home': 'Beranda',
'breadcrumb.profile': 'Profil',
'breadcrumb.scholarship': 'Beasiswa',
'hero.badge': 'Program Beasiswa',
'hero.title': 'Beasiswa Studi &<br>Penelitian',
'hero.subtitle': 'Teknik Informatika Universitas Tanjungpura',
'hero.desc': 'Temukan berbagai peluang beasiswa untuk mendukung perjalanan akademik dan penelitian Anda. Program Studi Informatika UNTAN berkomitmen membantu mahasiswa meraih prestasi terbaik.',
'hero.cta': 'Lihat Daftar Beasiswa',
'stats.total': 'Total Mahasiswa Penerima<br>Beasiswa Semester Ini',
'stats.gov': 'Total Penerima Beasiswa<br>Pembiayaan Pemerintah',
'stats.private': 'Total Penerima Beasiswa<br>Pembiayaan Swasta',
'dir.label': 'Daftar Beasiswa',
'dir.title': 'Daftar Beasiswa',
'dir.desc': 'Temukan beragam beasiswa dari berbagai sumber resmi yang tersedia untuk mahasiswa aktif, alumni, dan calon mahasiswa Program Studi Informatika UNTAN.',
'dir.receive': 'Terima Bantuan Beasiswa?',
'filter.all': 'SEMUA',
'filter.open': 'Sedang Buka',
'filter.closed': 'Ditutup',
'filter.gov': 'Pemerintah',
'filter.private': 'Swasta',
'search.placeholder': 'Cari Nama Beasiswa...',
'testimonial.title': 'Apa Kata Mereka?',
'testimonial.desc': 'Cerita inspiratif dari para penerima beasiswa Program Studi Informatika UNTAN',
'cta.title': 'Ingin Mengetahui Info Lebih Lanjut?',
'cta.desc': 'Hubungi admin prodi atau ikuti Instagram kemahasiswaan UNTAN untuk informasi terbaru tentang beasiswa dan program akademik.',
'cta.btn1': 'DAFTAR BEASISWA',
'cta.btn2': 'Hubungi Kami',
'ticker': 'Pendaftaran Beasiswa KIP Kuliah 2026 telah dibuka! &nbsp;—&nbsp; Beasiswa Pertamina Foundation open hingga Agustus 2026 &nbsp;—&nbsp; Daftarkan diri Anda sekarang! &nbsp;—&nbsp; Info lengkap tersedia di halaman direktori beasiswa &nbsp;—&nbsp; Beasiswa LPDP S2 masih tersedia hingga September 2026 &nbsp;—&nbsp;',
},
en: {
'nav.tentang': 'ABOUT US',
'nav.program': 'PROGRAMS',
'nav.sdm': 'HUMAN RESOURCES',
'nav.fasilitas': 'FACILITIES',
'nav.agenda': 'WORKS & EVENTS',
'nav.kontak': 'CONTACT',
'breadcrumb.home': 'Home',
'breadcrumb.profile': 'Profile',
'breadcrumb.scholarship': 'Scholarship',
'hero.badge': 'Scholarship Program',
'hero.title': 'Study & Research<br>Scholarships',
'hero.subtitle': 'Informatics Engineering, Tanjungpura University',
'hero.desc': 'Discover various scholarship opportunities to support your academic and research journey. The Informatics Study Program at UNTAN is committed to helping students achieve excellence.',
'hero.cta': 'View Scholarships',
'stats.total': 'Total Scholarship<br>Recipients This Semester',
'stats.gov': 'Total Government<br>Scholarship Recipients',
'stats.private': 'Total Private<br>Scholarship Recipients',
'dir.label': 'Scholarship Directory',
'dir.title': 'Scholarship Directory',
'dir.desc': 'Find various scholarships from official sources available for active students, alumni, and prospective students of the UNTAN Informatics Study Program.',
'dir.receive': 'Need Scholarship Aid?',
'filter.all': 'ALL',
'filter.open': 'Open',
'filter.closed': 'Closed',
'filter.gov': 'Government',
'filter.private': 'Private',
'search.placeholder': 'Search Scholarship Name...',
'testimonial.title': 'What They Say?',
'testimonial.desc': 'Inspiring stories from scholarship recipients at UNTAN Informatics Study Program',
'cta.title': 'Want to Know More?',
'cta.desc': 'Contact the program admin or follow UNTAN student affairs Instagram for the latest scholarship and academic program information.',
'cta.btn1': 'SCHOLARSHIP LIST',
'cta.btn2': 'Contact Us',
'ticker': 'KIP Kuliah Scholarship 2026 registration is now open! &nbsp;—&nbsp; Pertamina Foundation Scholarship open until August 2026 &nbsp;—&nbsp; Register now! &nbsp;—&nbsp; Full info available in the scholarship directory &nbsp;—&nbsp; LPDP S2 Scholarship available until September 2026 &nbsp;—&nbsp;',
}
};
// Load saved language from session storage
let currentLang = sessionStorage.getItem('lang') || 'id';
function applyTranslations(lang) {
const t = i18nData[lang];
if (!t) return;
// Apply data-i18n attributes
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (t[key] !== undefined) {
el.innerHTML = t[key];
}
});
// Apply data-i18n-placeholder attributes
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
if (t[key] !== undefined) {
el.placeholder = t[key];
}
});
// Update HTML lang attribute
document.documentElement.lang = lang === 'id' ? 'id' : 'en';
}
// Override setLanguage to delegate to localization.js when available,
// so both systems stay in sync (localization.js dispatches 'languageChanged'
// which triggers our listener below to re-apply dot-key translations).
function setLanguage(lang) {
if (lang !== 'id' && lang !== 'en') return;
if (typeof setLang === 'function') {
setLang(lang); // localization.js handles state + dispatches languageChanged
} else {
currentLang = lang;
sessionStorage.setItem('lang', lang);
applyTranslations(lang);
}
}
window.setLanguage = setLanguage;
// Re-apply dot-key translations whenever localization.js changes the language.
// This fixes the race where localization.js overwrites dot-key elements with the
// raw key string (its fallback for unknown keys).
document.addEventListener('languageChanged', (e) => {
applyTranslations(e.detail.lang);
});
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
// Use localization.js state if it's already loaded, otherwise fall back.
const initLang = (typeof getLang === 'function') ? getLang()
: (sessionStorage.getItem('lang') || 'id');
applyTranslations(initLang);
});
+8 -3
View File
@@ -18,9 +18,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Navbar sudah fixed di CSS; tidak perlu mengubah warna karena
// warna primary sudah menjadi identitas halaman ini.
// Namun kita tambahkan shadow saat scroll untuk depth effect.
const navbar = document.getElementById('navbar');
// PERBAIKAN: Menyesuaikan ID dengan yang ada di index.html (nav-wrapper)
const navbar = document.getElementById('nav-wrapper') || document.getElementById('navbar');
const handleScroll = () => {
if (!navbar) return; // PERBAIKAN: Mencegah error jika elemen tidak ada di halaman
if (window.scrollY > 10) {
navbar.style.boxShadow = '0 4px 20px rgba(0,0,0,0.25)';
} else {
@@ -78,7 +82,8 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault();
const navHeight = navbar.offsetHeight || 110;
// PERBAIKAN: Mencegah error saat membaca offsetHeight jika navbar null
const navHeight = navbar ? navbar.offsetHeight : 110;
const top = target.getBoundingClientRect().top + window.scrollY - navHeight - 4;
window.scrollTo({ top, behavior: 'smooth' });
@@ -165,4 +170,4 @@ document.addEventListener('DOMContentLoaded', () => {
if (contactClose) contactClose.addEventListener('click', closeContactModal);
if (contactBackdrop) contactBackdrop.addEventListener('click', closeContactModal);
}); // end DOMContentLoaded
}); // end DOMContentLoaded
+90 -38
View File
@@ -1,6 +1,7 @@
// =====================================================
// popup.js — Modal Detail Beasiswa
// Tutup via: tombol ×, klik overlay, tekan Escape
// + DUKUNGAN MULTI-BAHASA (ID/EN)
// =====================================================
const overlay = document.getElementById('popup-overlay');
@@ -8,32 +9,91 @@ const modal = document.getElementById('popup-modal');
const closeBtn = document.getElementById('popup-close');
const popupBody = document.getElementById('popup-content');
// Format tanggal ke format Indonesia
function fmtDate(str) {
let currentOpenId = null; // Menyimpan status beasiswa mana yang sedang dibuka
// Bertanya langsung ke mesin utama (localization.js)
function getPopupLang() {
if (typeof window.getLang === 'function') {
return window.getLang();
}
return localStorage.getItem('if_untan_lang') || 'id';
}
// Format tanggal dinamis (ID / EN)
function fmtDate(str, lang) {
if (!str) return '—';
const d = new Date(str);
if (isNaN(d.getTime())) return str;
return d.toLocaleDateString('id-ID', {
return d.toLocaleDateString(lang === 'en' ? 'en-US' : 'id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}
// Kamus Teks Statis Modal
const popupDict = {
id: {
req_empty: 'Syarat belum tersedia. Kunjungi sumber resmi.',
req_title: 'Syarat Umum',
open_date: 'Tanggal Buka',
close_date: 'Tanggal Tutup',
contact: 'Kontak',
more_info: 'Info Lebih Lanjut',
no_link: 'Tautan tidak tersedia. Silakan hubungi admin prodi.',
status_open: 'Buka',
status_closed: 'Ditutup'
},
en: {
req_empty: 'Requirements not available. Visit official source.',
req_title: 'General Requirements',
open_date: 'Open Date',
close_date: 'Closing Date',
contact: 'Contact',
more_info: 'More Information',
no_link: 'Link unavailable. Please contact department admin.',
status_open: 'Open',
status_closed: 'Closed'
}
};
/**
* Buka popup modal untuk beasiswa dengan ID tertentu
* Data diambil dari window.beasiswaData yang diset oleh filter.js
*/
function openPopup(id) {
currentOpenId = id;
const data = window.beasiswaData || [];
const bsw = data.find(b => b.id === id);
if (!bsw) return;
const lang = getPopupLang();
const d = popupDict[lang];
const isBuka = bsw.status === 'Buka' || bsw.status === 'Dibuka';
const badgeClass = isBuka ? 'badge-buka' : 'badge-ditutup';
// Terjemahkan Status & Kategori
const statusText = isBuka ? d.status_open : (lang === 'en' && (bsw.status === 'Ditutup' || bsw.status === 'Tutup') ? d.status_closed : bsw.status);
// Menerjemahkan Kategori (Misal: "Beasiswa Swasta" -> "Private Scholarship")
let kategoriText = bsw.kategori;
if (lang === 'en') {
if (bsw.kategori_en) {
kategoriText = bsw.kategori_en;
} else if (bsw.kategori.toLowerCase().includes('swasta')) {
kategoriText = 'Private Scholarship';
} else if (bsw.kategori.toLowerCase().includes('pemerintah')) {
kategoriText = 'Government Scholarship';
}
}
// Teks Dinamis (Ambil _en dari JSON jika ada, jika tidak pakai ID)
const namaText = (lang === 'en' && bsw.nama_en) ? bsw.nama_en : bsw.nama;
const deskripsiText = (lang === 'en' && bsw.deskripsi_singkat_en) ? bsw.deskripsi_singkat_en : bsw.deskripsi_singkat;
const syaratData = (lang === 'en' && bsw.syarat_umum_en) ? bsw.syarat_umum_en : (bsw.syarat_umum || []);
// Bangun list syarat umum
const syaratItems = (bsw.syarat_umum || [])
const syaratItems = syaratData
.map(s => `
<li>
<i class="fas fa-check-circle"></i>
@@ -43,74 +103,65 @@ function openPopup(id) {
const syaratHTML = syaratItems
? `<ul class="syarat-list">${syaratItems}</ul>`
: `<p style="font-size:13px;color:#94a3b8;font-style:italic;">
Syarat belum tersedia. Kunjungi sumber resmi.
</p>`;
: `<p style="font-size:13px;color:#94a3b8;font-style:italic;">${d.req_empty}</p>`;
const hasUrl = bsw.url_sumber && bsw.url_sumber !== '#';
// Render konten popup
// Render konten popup dengan template Anda yang dipertahankan
popupBody.innerHTML = `
<div class="popup-body">
<!-- Badge status + kategori -->
<div class="popup-kategori-row">
<span class="badge-status ${badgeClass}">${bsw.status}</span>
<span class="popup-kategori">${bsw.kategori}</span>
<span class="badge-status ${badgeClass}">${statusText}</span>
<span class="popup-kategori">${kategoriText}</span>
</div>
<!-- Judul -->
<h2 id="popup-title" class="popup-title">${bsw.nama}</h2>
<h2 id="popup-title" class="popup-title">${namaText}</h2>
<!-- Deskripsi singkat -->
<p class="popup-desc">${bsw.deskripsi_singkat}</p>
<p class="popup-desc">${deskripsiText}</p>
<!-- Syarat Umum -->
<div class="popup-section-title">
<i class="fas fa-list-check"></i>
Syarat Umum
${d.req_title}
</div>
${syaratHTML}
<!-- Periode Pendaftaran -->
<div class="popup-periode">
<div class="popup-periode-item">
<label>Tanggal Buka</label>
<span>${fmtDate(bsw.tanggal_buka)}</span>
<label>${d.open_date}</label>
<span>${fmtDate(bsw.tanggal_buka, lang)}</span>
</div>
<div class="popup-periode-item">
<label>Tanggal Tutup</label>
<span style="${!isBuka ? 'color:#EF4444;' : ''}">${fmtDate(bsw.tanggal_tutup)}</span>
<label>${d.close_date}</label>
<span style="${!isBuka ? 'color:#EF4444;' : ''}">${fmtDate(bsw.tanggal_tutup, lang)}</span>
</div>
</div>
<!-- Kontak -->
${bsw.kontak ? `
<div class="popup-kontak">
<i class="fas fa-phone-alt"></i>
<div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px;">Kontak</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px;">${d.contact}</div>
${bsw.kontak}
</div>
</div>` : ''}
<!-- Tombol Info Lebih Lanjut (UC-05) -->
${hasUrl
? `<a
href="${bsw.url_sumber}"
target="_blank"
rel="noopener noreferrer"
class="popup-cta"
aria-label="Info lebih lanjut tentang ${bsw.nama}"
aria-label="${d.more_info} ${bsw.nama}"
>
Info Lebih Lanjut
${d.more_info}
<i class="fas fa-external-link-alt" style="font-size:12px;"></i>
</a>`
: `<button class="popup-cta disabled" disabled aria-disabled="true">
Info Lebih Lanjut
${d.more_info}
<i class="fas fa-external-link-alt" style="font-size:12px;"></i>
</button>
<p style="text-align:center;font-size:11px;color:#94a3b8;margin-top:8px;">
Tautan tidak tersedia. Silakan hubungi admin prodi.
${d.no_link}
</p>`
}
</div>
@@ -126,25 +177,26 @@ function openPopup(id) {
/** Tutup popup modal */
function closePopup() {
currentOpenId = null;
overlay.classList.remove('show');
document.body.style.overflow = '';
}
// === Event Listeners untuk menutup popup ===
// 1. Tombol × close
closeBtn?.addEventListener('click', closePopup);
// 2. Klik di luar area modal (backdrop)
document.getElementById('popup-backdrop')?.addEventListener('click', closePopup);
// 3. Tekan Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('show')) {
closePopup();
}
});
// Expose ke global (dipanggil dari onclick di kartu)
// Jika bahasa diganti SAAT popup sedang terbuka, cetak ulang isinya!
document.addEventListener('languageChanged', () => {
if (currentOpenId && overlay.classList.contains('show')) {
openPopup(currentOpenId);
}
});
window.openPopup = openPopup;
window.closePopup = closePopup;
window.closePopup = closePopup;