forked from izu/student-web-if-development-kit
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:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 =>
|
||||
({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[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();
|
||||
});
|
||||
@@ -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! — Beasiswa Pertamina Foundation open hingga Agustus 2026 — Daftarkan diri Anda sekarang! — Info lengkap tersedia di halaman direktori beasiswa — Beasiswa LPDP S2 masih tersedia hingga September 2026 — ',
|
||||
},
|
||||
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! — Pertamina Foundation Scholarship open until August 2026 — Register now! — Full info available in the scholarship directory — LPDP S2 Scholarship available until September 2026 — ',
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user