forked from izu/student-web-if-development-kit
364 lines
15 KiB
JavaScript
364 lines
15 KiB
JavaScript
// ===== LANGUAGE TOGGLE =====
|
||
let currentLang = 'id';
|
||
|
||
function setLangLabel(lang) {
|
||
const nextLang = lang === 'id' ? 'EN' : 'ID';
|
||
const labelDesktop = document.getElementById('lang-label');
|
||
const labelMobile = document.getElementById('lang-label-mobile');
|
||
if (labelDesktop) labelDesktop.textContent = nextLang;
|
||
if (labelMobile) labelMobile.textContent = nextLang;
|
||
}
|
||
|
||
function applyLang(lang) {
|
||
const pageBody = document.getElementById('page-body');
|
||
pageBody.classList.toggle('lang-id', lang === 'id');
|
||
pageBody.classList.toggle('lang-en', lang === 'en');
|
||
document.documentElement.lang = lang;
|
||
}
|
||
|
||
function toggleLang() {
|
||
currentLang = currentLang === 'id' ? 'en' : 'id';
|
||
applyLang(currentLang);
|
||
setLangLabel(currentLang);
|
||
}
|
||
|
||
document.getElementById('lang-toggle')?.addEventListener('click', toggleLang);
|
||
document.getElementById('lang-toggle-mobile')?.addEventListener('click', toggleLang);
|
||
|
||
// ===== SCROLL REVEAL =====
|
||
function initScrollReveal() {
|
||
const revealEls = document.querySelectorAll('.reveal');
|
||
const observer = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('visible');
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.1 });
|
||
revealEls.forEach(el => observer.observe(el));
|
||
}
|
||
initScrollReveal();
|
||
|
||
// ===== BACK TO TOP =====
|
||
function initBackToTop() {
|
||
const btn = document.getElementById('back-to-top');
|
||
if (!btn) return;
|
||
|
||
btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
||
|
||
window.addEventListener('scroll', () => {
|
||
const isPastThreshold = window.scrollY > 400;
|
||
btn.classList.toggle('hidden', !isPastThreshold);
|
||
btn.classList.toggle('flex', isPastThreshold);
|
||
});
|
||
}
|
||
initBackToTop();
|
||
|
||
// ===== PRESTASI FILTER =====
|
||
function setActiveTabStyle(btn, isActive) {
|
||
btn.classList.toggle('bg-untan-navy', isActive);
|
||
btn.classList.toggle('text-white', isActive);
|
||
btn.classList.toggle('bg-white', !isActive);
|
||
btn.classList.toggle('text-slate-700', !isActive);
|
||
btn.classList.toggle('border', !isActive);
|
||
btn.classList.toggle('border-slate-200',!isActive);
|
||
}
|
||
|
||
function filterPrestasi(category) {
|
||
document.querySelectorAll('.prestasi-tab-btn').forEach(btn => {
|
||
setActiveTabStyle(btn, btn.dataset.tab === category);
|
||
});
|
||
document.querySelectorAll('#prestasi-grid .prestasi-card').forEach(card => {
|
||
card.style.display = (category === 'all' || card.dataset.category === category) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
function initPrestasiFilter() {
|
||
document.querySelectorAll('.prestasi-tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => filterPrestasi(btn.dataset.tab));
|
||
});
|
||
}
|
||
initPrestasiFilter();
|
||
|
||
// ===== MEGA MENU =====
|
||
(function initMegaMenu() {
|
||
const menuItems = document.querySelectorAll('[data-menu]');
|
||
if (!menuItems.length) return;
|
||
|
||
let activeMenuId = null;
|
||
|
||
function closeAllMenus() {
|
||
document.querySelectorAll('.mega-panel').forEach(p => p.classList.remove('active'));
|
||
updateButtonStates(null);
|
||
activeMenuId = null;
|
||
}
|
||
|
||
function updateButtonStates(openMenuId) {
|
||
menuItems.forEach(item => {
|
||
const isActive = item.getAttribute('data-menu') === openMenuId;
|
||
const btn = item.querySelector('.nav-menu-btn');
|
||
const chevron = item.querySelector('.fa-chevron-down');
|
||
if (btn) {
|
||
btn.classList.toggle('active', isActive);
|
||
btn.setAttribute('aria-expanded', isActive ? 'true' : 'false');
|
||
}
|
||
if (chevron) chevron.style.transform = isActive ? 'rotate(180deg)' : '';
|
||
});
|
||
}
|
||
|
||
menuItems.forEach(item => {
|
||
const menuId = item.getAttribute('data-menu');
|
||
const btn = item.querySelector('.nav-menu-btn');
|
||
if (!btn) return;
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
if (activeMenuId === menuId) {
|
||
closeAllMenus();
|
||
} else {
|
||
closeAllMenus();
|
||
updateButtonStates(menuId);
|
||
activeMenuId = menuId;
|
||
}
|
||
});
|
||
});
|
||
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAllMenus(); });
|
||
document.addEventListener('click', e => { if (!e.target.closest('#main-nav') && activeMenuId) closeAllMenus(); });
|
||
})();
|
||
|
||
// ===== MOBILE MENU =====
|
||
function initMobileMenu() {
|
||
const menuBtn = document.getElementById('mobile-menu-btn');
|
||
const menu = document.getElementById('mobile-menu');
|
||
const closeBtn = document.getElementById('close-mobile-menu');
|
||
|
||
if (menuBtn && menu) {
|
||
menuBtn.addEventListener('click', () => {
|
||
const isOpen = menu.classList.toggle('-translate-x-full') === false;
|
||
menuBtn.setAttribute('aria-expanded', String(isOpen));
|
||
});
|
||
}
|
||
if (closeBtn && menu) {
|
||
closeBtn.addEventListener('click', () => menu.classList.add('-translate-x-full'));
|
||
}
|
||
}
|
||
initMobileMenu();
|
||
|
||
// ===== LUCO CHATBOT BUBBLE =====
|
||
function initLucoBubble() {
|
||
const bubble = document.getElementById('luco-bubble');
|
||
const closeBtn = document.getElementById('luco-bubble-close');
|
||
|
||
if (!bubble) return;
|
||
|
||
setTimeout(() => bubble.classList.remove('hidden'), 3000);
|
||
|
||
closeBtn?.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
bubble.classList.add('hidden');
|
||
});
|
||
}
|
||
initLucoBubble();
|
||
|
||
// ===== #ASSIGNMENT-BODY ENHANCEMENTS =====
|
||
(function initAssignmentBody() {
|
||
const body = document.getElementById('assignment-body');
|
||
if (!body) return;
|
||
|
||
// --- 1. Scroll-reveal untuk setiap child langsung ---
|
||
const revealTargets = body.querySelectorAll('h3, h4, p, ul, table, figure');
|
||
const revealObserver = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry, i) => {
|
||
if (entry.isIntersecting) {
|
||
setTimeout(() => entry.target.classList.add('visible'), i * 60);
|
||
revealObserver.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.08 });
|
||
|
||
revealTargets.forEach(el => {
|
||
el.classList.add('ab-reveal');
|
||
revealObserver.observe(el);
|
||
});
|
||
|
||
// --- 2. Collapsible: Alumni & Mitra (sections yang panjang) ---
|
||
// Wrap konten setelah h3 "Kisah Sukses Alumni" dalam collapsible
|
||
const allH3 = [...body.querySelectorAll('h3')];
|
||
const collapsibleTitles = ['Kisah Sukses Alumni', 'Mitra Strategis', 'Fasilitas Akademik'];
|
||
|
||
allH3.forEach(h3 => {
|
||
if (!collapsibleTitles.includes(h3.textContent.trim())) return;
|
||
|
||
// Kumpulkan sibling sampai h3 berikutnya
|
||
const siblings = [];
|
||
let next = h3.nextElementSibling;
|
||
while (next && next.tagName !== 'H3') {
|
||
siblings.push(next);
|
||
next = next.nextElementSibling;
|
||
}
|
||
if (!siblings.length) return;
|
||
|
||
// Buat wrapper collapsible
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'ab-collapsible';
|
||
h3.parentNode.insertBefore(wrapper, siblings[0]);
|
||
siblings.forEach(s => wrapper.appendChild(s));
|
||
|
||
// Buat tombol toggle
|
||
const btn = document.createElement('button');
|
||
btn.className = 'ab-readmore-btn';
|
||
btn.innerHTML = 'Lihat Detail <span class="arrow">▼</span>';
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
h3.insertAdjacentElement('afterend', btn);
|
||
|
||
btn.addEventListener('click', () => {
|
||
const isOpen = wrapper.classList.toggle('open');
|
||
btn.classList.toggle('open', isOpen);
|
||
btn.innerHTML = isOpen
|
||
? 'Sembunyikan <span class="arrow">▼</span>'
|
||
: 'Lihat Detail <span class="arrow">▼</span>';
|
||
btn.setAttribute('aria-expanded', String(isOpen));
|
||
});
|
||
});
|
||
|
||
// --- 3. Table row highlight on click ---
|
||
body.querySelectorAll('tbody tr').forEach(row => {
|
||
row.style.cursor = 'pointer';
|
||
row.addEventListener('click', () => {
|
||
body.querySelectorAll('tbody tr.selected').forEach(r => r.classList.remove('selected'));
|
||
row.classList.add('selected');
|
||
row.style.background = '#dbeafe';
|
||
row.style.fontWeight = '600';
|
||
setTimeout(() => {
|
||
row.style.background = '';
|
||
row.style.fontWeight = '';
|
||
row.classList.remove('selected');
|
||
}, 1800);
|
||
});
|
||
});
|
||
|
||
// --- 4. Callout block: inject setelah paragraf karir ---
|
||
const pEls = [...body.querySelectorAll('p')];
|
||
const kariPara = pEls.find(p => p.textContent.includes('Rp 5–12 Juta'));
|
||
if (kariPara) {
|
||
const callout = document.createElement('div');
|
||
callout.className = 'ab-callout ab-reveal';
|
||
callout.textContent = '87% lulusan bekerja di bidang IT sesuai kompetensi, dengan rata-rata waktu tunggu kerja kurang dari 3 bulan.';
|
||
kariPara.insertAdjacentElement('afterend', callout);
|
||
revealObserver.observe(callout);
|
||
}
|
||
|
||
// --- 5. Tambahkan section label di atas setiap h3 ---
|
||
const sectionMap = {
|
||
'Mengenal Program Studi': 'Tentang Prodi',
|
||
'Visi': 'Identitas',
|
||
'Misi': 'Identitas',
|
||
'Tonggak Sejarah': 'Sejarah',
|
||
'dalam Angka': 'Data & Statistik',
|
||
'Pencapaian': 'Prestasi',
|
||
'Study Club': 'Komunitas',
|
||
'Prospek Karir': 'Karir',
|
||
'Kisah Sukses': 'Alumni',
|
||
'Fasilitas': 'Infrastruktur',
|
||
'Mitra Strategis': 'Kerjasama',
|
||
'Informasi Kontak': 'Kontak',
|
||
};
|
||
allH3.forEach(h3 => {
|
||
const key = Object.keys(sectionMap).find(k => h3.textContent.includes(k));
|
||
if (!key) return;
|
||
const label = document.createElement('div');
|
||
label.className = 'ab-section-label';
|
||
label.textContent = sectionMap[key];
|
||
h3.insertAdjacentElement('beforebegin', label);
|
||
});
|
||
|
||
})();
|
||
|
||
// =====================================================
|
||
// JELLYFISH.CORP — MOCKUP SCRIPTS
|
||
// =====================================================
|
||
|
||
// ===== PRESTASI FILTER (new jf-* classes) =====
|
||
(function initPrestasiFilterJF() {
|
||
const tabs = document.querySelectorAll('.jf-tab-btn');
|
||
const cards = document.querySelectorAll('#prestasi-grid .jf-prestasi-card');
|
||
if (!tabs.length) return;
|
||
|
||
tabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
tabs.forEach(t => t.classList.remove('jf-tab-active'));
|
||
tab.classList.add('jf-tab-active');
|
||
const filter = tab.dataset.filter;
|
||
cards.forEach(card => {
|
||
const show = filter === 'all' || card.dataset.cat === filter;
|
||
card.style.display = show ? '' : 'none';
|
||
if (show) {
|
||
card.style.animation = 'jfFadeIn 0.35s ease forwards';
|
||
}
|
||
});
|
||
});
|
||
});
|
||
})();
|
||
|
||
// ===== SCROLL REVEAL for jf-* cards =====
|
||
(function initJFScrollReveal() {
|
||
const targets = document.querySelectorAll(
|
||
'.jf-stat-card, .jf-stat-row-card, .jf-prestasi-card, ' +
|
||
'.jf-sc-card, .jf-karir-card, .jf-alumni-card, ' +
|
||
'.jf-infra-card, .jf-mitra-card, .jf-life-card, ' +
|
||
'.jf-tl-item, .jf-visi-card, .jf-kontak-item'
|
||
);
|
||
targets.forEach(el => {
|
||
el.style.opacity = '0';
|
||
el.style.transform = 'translateY(24px)';
|
||
el.style.transition = 'opacity 0.55s ease, transform 0.55s ease';
|
||
});
|
||
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach((entry, i) => {
|
||
if (entry.isIntersecting) {
|
||
setTimeout(() => {
|
||
entry.target.style.opacity = '1';
|
||
entry.target.style.transform = 'translateY(0)';
|
||
}, i * 70);
|
||
obs.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.08 });
|
||
|
||
targets.forEach(el => obs.observe(el));
|
||
})();
|
||
|
||
// ===== ANIMATED COUNTER for jf-stat-num =====
|
||
(function initJFCounters() {
|
||
const nums = document.querySelectorAll('.jf-stat-num');
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach(entry => {
|
||
if (!entry.isIntersecting) return;
|
||
const el = entry.target;
|
||
const raw = el.textContent.trim();
|
||
const num = parseFloat(raw.replace(/[^0-9.]/g, ''));
|
||
if (isNaN(num) || num === 0) return;
|
||
const suffix = raw.replace(/[0-9.,]/g, '');
|
||
const prefix = raw.startsWith('<') ? '<' : '';
|
||
const cleanSuffix = prefix ? suffix : suffix;
|
||
let start = 0;
|
||
const duration = 1400;
|
||
const step = 16;
|
||
const increment = num / (duration / step);
|
||
const timer = setInterval(() => {
|
||
start += increment;
|
||
if (start >= num) { start = num; clearInterval(timer); }
|
||
const display = num % 1 === 0
|
||
? Math.floor(start).toLocaleString('id-ID')
|
||
: start.toFixed(0);
|
||
el.textContent = prefix + display + cleanSuffix;
|
||
}, step);
|
||
obs.unobserve(el);
|
||
});
|
||
}, { threshold: 0.5 });
|
||
nums.forEach(el => obs.observe(el));
|
||
})(); |