feat(ApeNameTeamE): tambah proyek web-beasiswa-untan

Memindahkan seluruh file web-beasiswa-untan (assets, data, index.html,
SRS/SDD, PROJECT_CONTEXT) ke dalam folder grup ApeNameTeamE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Araya's Project
2026-05-22 13:33:40 +07:00
parent 0ebc25809b
commit 8e9137f725
17 changed files with 4256 additions and 1306 deletions
+109
View File
@@ -0,0 +1,109 @@
// =====================================================
// carousel.js — Swiper.js Carousel Testimoni
// Touch-friendly, auto-play, responsive breakpoints
// =====================================================
let testimoniSwiper = null;
/** Render slide testimoni ke dalam swiper-wrapper */
function renderTestimoniSlides(data) {
const wrapper = document.getElementById('testimonial-wrapper');
if (!wrapper) return;
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()}
</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('');
}
/** Inisialisasi Swiper setelah slide di-render */
function initSwiper(count) {
if (testimoniSwiper) {
testimoniSwiper.destroy(true, true);
testimoniSwiper = null;
}
testimoniSwiper = new Swiper('.testimonial-swiper', {
slidesPerView: 1,
spaceBetween: 24,
loop: count > 3,
centerInsufficientSlides: true,
autoplay: {
delay: 4500,
disableOnInteraction: false,
pauseOnMouseEnter: true,
},
pagination: {
el: '.testimonial-pagination',
clickable: true,
},
grabCursor: true,
breakpoints: {
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 },
},
a11y: {
prevSlideMessage: 'Slide sebelumnya',
nextSlideMessage: 'Slide berikutnya',
},
});
}
/** Fetch data testimoni dan render */
async function initCarousel() {
const section = document.getElementById('testimonial');
try {
const res = await fetch('data/testimoni.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
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);
+53
View File
@@ -0,0 +1,53 @@
// =====================================================
// counter.js — Animasi Counter Statistik
// Menggunakan IntersectionObserver + requestAnimationFrame
// =====================================================
/**
* Animasi angka dari 0 ke nilai target
* @param {HTMLElement} el - Elemen yang menampilkan angka
* @param {number} target - Angka target
* @param {number} duration - Durasi animasi dalam ms
*/
function animateCounter(el, target, duration = 1800) {
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease out cubic — percepat di awal, lambat di akhir
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(eased * target);
el.textContent = current;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
function initCounters() {
const counters = document.querySelectorAll('[data-counter]');
if (!counters.length) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !entry.target.dataset.animated) {
entry.target.dataset.animated = 'true';
const target = parseInt(entry.target.dataset.counter, 10) || 0;
animateCounter(entry.target, target);
}
});
},
{ threshold: 0.6 }
);
counters.forEach(el => observer.observe(el));
}
document.addEventListener('DOMContentLoaded', initCounters);
+242
View File
@@ -0,0 +1,242 @@
// =====================================================
// filter.js — Fetch JSON, Render Kartu, Filter Tab, Pencarian, Paginasi
// Filter dan search berjalan KUMULATIF (sesuai PROJECT_CONTEXT rule #6)
// =====================================================
const ITEMS_PER_PAGE = 6; // 2 baris × 3 kolom desktop
let allData = []; // Semua data beasiswa dari JSON
let activeFilter = 'semua';
let searchKeyword = '';
let currentPage = 1;
// Format tanggal
function formatDate(str) {
if (!str) return '—';
const d = new Date(str);
if (isNaN(d.getTime())) return str;
return d.toLocaleDateString('id-ID', {
day: 'numeric', month: 'long', year: 'numeric'
});
}
// Sanitasi string untuk mencegah XSS pada search
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) {
const isBuka = bsw.status === 'Buka';
const article = document.createElement('article');
article.className = 'scholarship-card fade-in-up';
article.setAttribute('role', 'listitem');
article.setAttribute('data-id', bsw.id);
article.innerHTML = `
<div class="card-img-wrap">
<img
src="${bsw.thumbnail}"
alt="${bsw.nama}"
class="card-img"
loading="lazy"
onerror="this.style.display='none';this.parentElement.style.background='#eef2ff';this.parentElement.innerHTML+='<i class=\\'fas fa-graduation-cap\\' style=\\'font-size:40px;color:#c7d2fe;\\'></i>'"
>
</div>
<div class="card-body">
<span class="badge-status ${isBuka ? 'badge-buka' : 'badge-ditutup'}">
${bsw.status}
</span>
<h3 class="card-title">${bsw.nama}</h3>
<p class="card-desc">${bsw.deskripsi_singkat}</p>
<div class="card-date">
<i class="fas fa-calendar-alt"></i>
<span>${formatDate(bsw.tanggal_tutup)}</span>
</div>
</div>
<div class="card-footer">
<button
class="btn-selengkapnya"
onclick="openPopup('${bsw.id}')"
aria-label="Lihat detail beasiswa ${bsw.nama}"
>
Selengkapnya <i class="fas fa-arrow-right" style="font-size:10px;"></i>
</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;
// 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));
return matchFilter && matchSearch;
});
}
/** Render kartu + paginasi ke DOM */
function renderCards() {
const grid = document.getElementById('beasiswa-grid');
const pagCont = document.getElementById('pagination');
const filtered = getFiltered();
// Bersihkan grid
grid.innerHTML = '';
if (filtered.length === 0) {
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>
</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`;
grid.appendChild(card);
});
renderPagination(totalPages, pagCont);
}
/** Render tombol-tombol paginasi */
function renderPagination(total, container) {
container.innerHTML = '';
if (total <= 1) return;
const make = (content, page, disabled = false, isActive = false) => {
const btn = document.createElement('button');
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;
window.scrollTo({ top, behavior: 'smooth' });
}
});
}
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', () => {
document.querySelectorAll('.filter-tab').forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
activeFilter = btn.dataset.filter;
currentPage = 1;
renderCards();
});
});
}
/** 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 JSON dan render */
async function initDirectory() {
const grid = document.getElementById('beasiswa-grid');
try {
const res = await fetch('data/beasiswa.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
allData = await res.json();
// Simpan ke window agar popup.js bisa akses
window.beasiswaData = allData;
renderCards();
} catch (err) {
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;">
Pastikan menggunakan live server. Silakan refresh atau hubungi admin.
</p>
</div>`;
}
}
document.addEventListener('DOMContentLoaded', () => {
initFilterTabs();
initSearch();
initDirectory();
});
+128
View File
@@ -0,0 +1,128 @@
// =====================================================
// 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';
}
function setLanguage(lang) {
if (lang !== 'id' && lang !== 'en') return;
currentLang = lang;
sessionStorage.setItem('lang', lang);
// Update button visual states
document.querySelectorAll('.lang-btn').forEach(btn => {
const isActive = btn.dataset.lang === lang;
btn.classList.toggle('active', isActive);
});
applyTranslations(lang);
}
// Expose globally so onclick in HTML works
window.setLanguage = setLanguage;
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
setLanguage(currentLang);
});
+177
View File
@@ -0,0 +1,177 @@
// =====================================================
// main.js — Inisialisasi Utama
// Sticky Navbar, Hamburger Menu, Smooth Scroll, AOS, Active Nav
// =====================================================
document.addEventListener('DOMContentLoaded', () => {
// ─── AOS (Animate on Scroll) ────────────────────────
AOS.init({
duration: 700,
easing: 'ease-out-cubic',
once: true,
offset: 50,
});
// ─── Sticky Navbar ──────────────────────────────────
// 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');
const handleScroll = () => {
if (window.scrollY > 10) {
navbar.style.boxShadow = '0 4px 20px rgba(0,0,0,0.25)';
} else {
navbar.style.boxShadow = 'none';
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // initial call
// ─── Ticker Visibility (Muncul setelah Hero & Stats) ─
const ticker = document.querySelector('.ticker-wrapper');
const statsSection = document.getElementById('statistics');
const handleTickerVisibility = () => {
if (!ticker || !statsSection) return;
// Ticker muncul setelah melewati batas bawah elemen statistik
const statsBottom = statsSection.offsetTop + statsSection.offsetHeight;
if (window.scrollY > statsBottom) {
ticker.classList.add('show-ticker');
} else {
ticker.classList.remove('show-ticker');
}
};
window.addEventListener('scroll', handleTickerVisibility, { passive: true });
// Call once on load with a slight delay to ensure layout is ready
setTimeout(handleTickerVisibility, 100);
// ─── Hamburger Menu (Mobile) ─────────────────────────
const hamburger = document.getElementById('hamburger');
const mobileMenu = document.getElementById('mobile-menu');
hamburger?.addEventListener('click', () => {
const isOpen = !mobileMenu.classList.contains('hidden');
mobileMenu.classList.toggle('hidden');
// Toggle icon
const icon = hamburger.querySelector('i');
if (isOpen) {
icon.className = 'fas fa-bars text-lg';
} else {
icon.className = 'fas fa-times text-lg';
}
// Aksesibilitas
hamburger.setAttribute('aria-expanded', String(!isOpen));
});
// ─── Smooth Scroll untuk anchor links ──────────────
// Sesuai SDD: CSS scroll-behavior + JS fallback dengan offset
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', e => {
const href = anchor.getAttribute('href');
const target = document.querySelector(href);
if (!target) return;
e.preventDefault();
const navHeight = navbar.offsetHeight || 110;
const top = target.getBoundingClientRect().top + window.scrollY - navHeight - 4;
window.scrollTo({ top, behavior: 'smooth' });
// Tutup mobile menu jika sedang terbuka
mobileMenu?.classList.add('hidden');
const icon = hamburger?.querySelector('i');
if (icon) icon.className = 'fas fa-bars text-lg';
});
});
// ─── Active Nav Link via IntersectionObserver ──────
// Sesuai SDD fitur #4: highlight menu sesuai seksi yang terlihat
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('a.nav-link');
const sectionObserver = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${entry.target.id}`) {
link.classList.add('active');
}
});
}
});
},
{ rootMargin: '-30% 0px -65% 0px' }
);
sections.forEach(s => sectionObserver.observe(s));
// ─── Back to Top visibility ────────────────────────
const backToTop = document.getElementById('back-to-top');
if (backToTop) {
window.addEventListener('scroll', () => {
if (window.scrollY > 400) {
backToTop.classList.remove('opacity-0', 'pointer-events-none');
backToTop.classList.add('opacity-100');
} else {
backToTop.classList.add('opacity-0', 'pointer-events-none');
backToTop.classList.remove('opacity-100');
}
}, { passive: true });
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// ─── Contact Us Modal (Hubungi Kami) ────────────────
const btnOpenContact = document.getElementById('btn-open-contact');
const contactOverlay = document.getElementById('contact-overlay');
const contactModal = document.getElementById('contact-modal');
const contactClose = document.getElementById('contact-close');
const contactBackdrop = document.getElementById('contact-backdrop');
const openContactModal = () => {
if (!contactOverlay) return;
contactOverlay.classList.remove('hidden');
contactOverlay.classList.add('flex');
// Animasi muncul
setTimeout(() => {
contactOverlay.classList.remove('opacity-0');
contactModal.classList.remove('scale-95');
contactModal.classList.add('scale-100');
}, 10);
};
const closeContactModal = () => {
if (!contactOverlay) return;
contactOverlay.classList.add('opacity-0');
contactModal.classList.remove('scale-100');
contactModal.classList.add('scale-95');
// Tunggu animasi selesai baru di-hide
setTimeout(() => {
contactOverlay.classList.remove('flex');
contactOverlay.classList.add('hidden');
}, 300);
};
if (btnOpenContact) btnOpenContact.addEventListener('click', openContactModal);
if (contactClose) contactClose.addEventListener('click', closeContactModal);
if (contactBackdrop) contactBackdrop.addEventListener('click', closeContactModal);
}); // end DOMContentLoaded
+158
View File
@@ -0,0 +1,158 @@
// =====================================================
// popup.js — Modal Detail Beasiswa
// Tutup via: tombol ×, klik overlay, tekan Escape
// =====================================================
const overlay = document.getElementById('popup-overlay');
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) {
if (!str) return '—';
const d = new Date(str);
if (isNaN(d.getTime())) return str;
return d.toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}
/**
* Buka popup modal untuk beasiswa dengan ID tertentu
* Data diambil dari window.beasiswaData yang diset oleh filter.js
*/
function openPopup(id) {
const data = window.beasiswaData || [];
const bsw = data.find(b => b.id === id);
if (!bsw) return;
const isBuka = bsw.status === 'Buka';
const badgeClass = isBuka ? 'badge-buka' : 'badge-ditutup';
// Bangun list syarat umum
const syaratItems = (bsw.syarat_umum || [])
.map(s => `
<li>
<i class="fas fa-check-circle"></i>
<span>${s}</span>
</li>`)
.join('');
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>`;
const hasUrl = bsw.url_sumber && bsw.url_sumber !== '#';
// Render konten popup
popupBody.innerHTML = `
<!-- Thumbnail / Hero gambar -->
<img
src="${bsw.thumbnail || ''}"
alt="${bsw.nama}"
class="popup-hero"
onerror="this.style.display='none'"
>
<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>
</div>
<!-- Judul -->
<h2 id="popup-title" class="popup-title">${bsw.nama}</h2>
<!-- Deskripsi singkat -->
<p class="popup-desc">${bsw.deskripsi_singkat}</p>
<!-- Syarat Umum -->
<div class="popup-section-title">
<i class="fas fa-list-check"></i>
Syarat Umum
</div>
${syaratHTML}
<!-- Periode Pendaftaran -->
<div class="popup-periode">
<div class="popup-periode-item">
<label>Tanggal Buka</label>
<span>${fmtDate(bsw.tanggal_buka)}</span>
</div>
<div class="popup-periode-item">
<label>Tanggal Tutup</label>
<span style="${!isBuka ? 'color:#EF4444;' : ''}">${fmtDate(bsw.tanggal_tutup)}</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>
${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}"
>
Info Lebih Lanjut
<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
<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.
</p>`
}
</div>
`;
// Tampilkan overlay
overlay.classList.add('show');
document.body.style.overflow = 'hidden';
// Set focus ke modal untuk aksesibilitas
setTimeout(() => modal.focus(), 50);
}
/** Tutup popup modal */
function closePopup() {
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)
window.openPopup = openPopup;
window.closePopup = closePopup;