/** * main.js — ACAAB Prestasi & Karya Mahasiswa * Vanilla JS: fetch data Directus CMS, render sections, handle interactions. */ // ─── Constants ──────────────────────────────────────────────────────────────── const API_BASE = 'https://api.ifuntanhub.dev'; const LANG_KEY = 'if_untan_lang'; const BIDANG_MAP = { 1: 'Kecerdasan Buatan', 2: 'Cyber Security', 3: 'Data', 4: 'Software Engineering', 5: 'Competitive Programming', 6: 'IoT', 7: 'Smart City', 8: 'Web3', 9: 'Karya Tulis Ilmiah', 10: 'Robotik', }; // ─── Utilities ──────────────────────────────────────────────────────────────── function escHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getLang() { try { return localStorage.getItem(LANG_KEY) || (navigator.language && navigator.language.startsWith('en') ? 'en' : 'id'); } catch (_) { return 'id'; } } // Baca dari translations global yang didefinisikan localization.js function t(key) { try { const lang = getLang(); if (typeof translations !== 'undefined' && translations[lang] && translations[lang][key]) { return translations[lang][key]; } } catch (_) {} return key; } // ─── Data Transformation ────────────────────────────────────────────────────── function extractRank(title) { const m = (title || '').match(/(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)/i); return m ? m[0] : 'Pemenang'; } function extractLevel(title, category) { const text = ((title || '') + ' ' + (category || '')).toLowerCase(); if (/internasional|international|global|world|asean|unesco/.test(text)) return 'Internasional'; if (/regional|kalbar|kalimantan|provinsi|kota|kabupaten|lokal/.test(text)) return 'Regional'; return 'Nasional'; } function extractCompetition(title) { return (title || '') .replace(/^(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)\s+(di\s+|pada\s+|dalam\s+)?/i, '') .trim(); } function getImageUrl(item, seed) { if (item.galeri && Array.isArray(item.galeri) && item.galeri.length > 0) { const first = item.galeri[0]; const imgId = typeof first === 'string' ? first : (first.directus_files_id || first.id); if (imgId) return `${API_BASE}/assets/${imgId}`; } if (item.foto) return `${API_BASE}/assets/${item.foto}`; return `https://picsum.photos/seed/${seed}/800/600`; } function transformAchievement(item) { const id = String(item.id); const title = item.nama_prestasi || 'Untitled Achievement'; let category = 'Umum'; if (item.bidang_id) { category = (typeof item.bidang_id === 'object' && item.bidang_id.nama_bidang) ? item.bidang_id.nama_bidang : (BIDANG_MAP[item.bidang_id] || 'Umum'); } const rank = extractRank(title); const level = extractLevel(title, category); const competition = extractCompetition(title); const year = parseInt(item.tahun) || new Date().getFullYear(); const anggota = Array.isArray(item.anggota) ? item.anggota : []; const team = item.nama_tim || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa'); const avatar = (item.nama_tim || title || 'IF').substring(0, 2).toUpperCase(); const description = item.deskripsi || `${team} berhasil meraih prestasi luar biasa sebagai ${rank} dalam ajang ${competition} tahun ${year} pada bidang ${category.toLowerCase()}.`; return { id, title, category, competition, rank, year, level, description, team, nim: item.nim || '', generation: item.angkatan ? `Angkatan ${item.angkatan}` : 'Informatika', image: getImageUrl(item, `prestasi-${id}`), avatar, members: anggota.map(m => ({ nama: m.nama || '', angkatan: m.angkatan ? parseInt(m.angkatan) : undefined })), }; } function transformProject(item) { const id = String(item.id); const title = item.nama_proyek || 'Untitled Project'; const anggota = Array.isArray(item.anggota) ? item.anggota : []; const studentName = item.nama_mahasiswa || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa'); const year = parseInt(item.tahun) || new Date().getFullYear(); const description = item.deskripsi || 'Tidak ada deskripsi.'; let categories = []; if (item.bidang && Array.isArray(item.bidang)) { categories = item.bidang.map(b => { if (b.bidang_id && typeof b.bidang_id === 'object' && b.bidang_id.nama_bidang) return b.bidang_id.nama_bidang; if (typeof b.bidang_id === 'number') return BIDANG_MAP[b.bidang_id] || 'Umum'; return 'Umum'; }).filter(Boolean); } if (categories.length === 0) categories = ['Umum']; let link = item.url || null; if (link && !/^https?:\/\//i.test(link)) link = `https://${link}`; return { id, title, studentName, year, description, categories, link, nim: item.nim || '', image: getImageUrl(item, `project-${id}`), catatan: item.catatan || '', members: anggota.map(m => ({ nama: m.nama || '', nim: m.nim || '' })), }; } // ─── API Fetch ──────────────────────────────────────────────────────────────── async function fetchAchievements() { const res = await fetch(`${API_BASE}/items/prestasi?fields=*.*`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); return (data || []) .filter(item => item.status === 'published') .map(transformAchievement); } async function fetchProjects() { const res = await fetch(`${API_BASE}/items/portfolio?fields=*,bidang.bidang_id.nama_bidang,galeri.*`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { data } = await res.json(); return (data || []) .filter(item => item.status === 'published') .map(transformProject); } // ─── App State ──────────────────────────────────────────────────────────────── let achievements = []; let projects = []; let achVisible = 3; let projVisible = 6; let achFilter = { search: '', level: '', cat: '', year: '' }; let projFilter = { search: '', cat: '', year: '' }; // ─── Init ───────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', async () => { initWaButton(); // Loading placeholders showSkeleton('acaab-ach-grid', 3); showSkeleton('acaab-proj-track', 4); const [achResult, projResult] = await Promise.allSettled([ fetchAchievements(), fetchProjects(), ]); if (achResult.status === 'fulfilled') { achievements = achResult.value.sort((a, b) => b.year - a.year); } else { console.error('[ACAAB] achievements error:', achResult.reason); showError('acaab-ach-grid', t('grp_acaab_ach_error')); } if (projResult.status === 'fulfilled') { projects = projResult.value; } else { console.error('[ACAAB] projects error:', projResult.reason); showError('acaab-proj-track', t('grp_acaab_proj_error')); } initStats(); initAchievements(); initProjects(); updateDataNote(); }); // Re-render saat bahasa berubah dari kit navbar document.addEventListener('languageChanged', () => { updateStats(); renderAchievements(); buildAchFilterOptions(); renderProjects(); buildProjFilterOptions(); updateWaButton(); updateDataNote(); updateSearchPlaceholders(); }); // ─── Skeleton / Error ───────────────────────────────────────────────────────── function showSkeleton(containerId, count) { const el = document.getElementById(containerId); if (!el) return; el.innerHTML = Array(count).fill(0).map(() => `
` ).join(''); } function showError(containerId, msg) { const el = document.getElementById(containerId); if (el) el.innerHTML = `

${escHtml(msg)}

`; } // ─── Stats ──────────────────────────────────────────────────────────────────── function initStats() { updateStats(); const statsEl = document.getElementById('acaab-stats'); if (!statsEl) return; const intl = achievements.filter(a => a.level === 'Internasional').length; const national = achievements.filter(a => a.level === 'Nasional').length; const regional = achievements.filter(a => a.level === 'Regional').length; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (!entry.isIntersecting) return; animateCount('stat-total-ach', achievements.length, 1600); animateCount('stat-total-proj', projects.length, 1600); animateCount('stat-intl', intl, 1200); animateCount('stat-national', national, 1350); animateCount('stat-regional', regional, 1500); observer.disconnect(); }); }, { threshold: 0.4 }); observer.observe(statsEl); } function updateStats() { const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; }; set('stat-total-ach', achievements.length); set('stat-total-proj', projects.length); set('stat-intl', achievements.filter(a => a.level === 'Internasional').length); set('stat-national', achievements.filter(a => a.level === 'Nasional').length); set('stat-regional', achievements.filter(a => a.level === 'Regional').length); } function animateCount(elId, target, duration) { const el = document.getElementById(elId); if (!el) return; const start = performance.now(); (function step(now) { const p = Math.min((now - start) / duration, 1); const eased = 1 - Math.pow(1 - p, 4); // easeOutQuart el.textContent = Math.round(target * eased).toLocaleString(); if (p < 1) requestAnimationFrame(step); })(start); } function updateDataNote() { const el = document.getElementById('acaab-data-note'); if (!el) return; const lang = getLang(); const locale = lang === 'en' ? 'en-US' : 'id-ID'; const date = new Date().toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' }); el.textContent = `${t('grp_acaab_stats_data_note')} ${date}`; } // ─── Achievements ───────────────────────────────────────────────────────────── const ACH_INIT = 3; const ACH_MORE = 3; const LEVEL_CLASS = { Internasional: 'acaab-badge-intl', Nasional: 'acaab-badge-nasional', Regional: 'acaab-badge-regional', }; function initAchievements() { buildAchFilterOptions(); renderAchievements(); bindAchEvents(); } function buildAchFilterOptions() { const levelSel = document.getElementById('acaab-ach-level'); const catSel = document.getElementById('acaab-ach-cat'); const yearSel = document.getElementById('acaab-ach-year'); if (!levelSel || !catSel || !yearSel) return; const levels = ['Internasional', 'Nasional', 'Regional']; const cats = [...new Set(achievements.map(a => a.category))].sort(); const years = [...new Set(achievements.map(a => String(a.year)))].filter(Boolean).sort((a, b) => b - a); levelSel.innerHTML = `` + levels.map(l => ``).join(''); catSel.innerHTML = `` + cats.map(c => ``).join(''); yearSel.innerHTML = `` + years.map(y => ``).join(''); } function filterAchievements() { const q = achFilter.search.toLowerCase(); return achievements.filter(a => { const matchSearch = !q || [a.title, a.team, a.category, a.competition] .some(s => (s || '').toLowerCase().includes(q)); const matchLevel = !achFilter.level || a.level === achFilter.level; const matchCat = !achFilter.cat || a.category === achFilter.cat; const matchYear = !achFilter.year || String(a.year) === achFilter.year; return matchSearch && matchLevel && matchCat && matchYear; }); } function renderAchievements() { const grid = document.getElementById('acaab-ach-grid'); const countEl = document.getElementById('acaab-ach-count'); const moreBtn = document.getElementById('acaab-ach-more'); const lessBtn = document.getElementById('acaab-ach-less'); const resetBtn= document.getElementById('acaab-ach-reset'); if (!grid) return; const filtered = filterAchievements(); const visible = filtered.slice(0, achVisible); const hasFilter = achFilter.search || achFilter.level || achFilter.cat || achFilter.year; if (countEl) countEl.textContent = `${t('grp_acaab_ach_showing')} ${filtered.length} ${t('grp_acaab_ach_results')}`; if (resetBtn) resetBtn.style.display = hasFilter ? 'flex' : 'none'; if (filtered.length === 0) { grid.innerHTML = `

${escHtml(t('grp_acaab_ach_empty'))}

`; if (moreBtn) moreBtn.style.display = 'none'; if (lessBtn) lessBtn.style.display = 'none'; return; } grid.innerHTML = visible.map((a, i) => achCardHtml(a, i)).join(''); if (moreBtn) moreBtn.style.display = achVisible < filtered.length ? 'inline-block' : 'none'; if (lessBtn) lessBtn.style.display = achVisible > ACH_INIT ? 'inline-block' : 'none'; grid.querySelectorAll('.acaab-ach-card').forEach(card => { card.addEventListener('click', (e) => { // Biarkan tombol yang merupakan link tetap berfungsi if (e.target.closest('a')) return; const ach = achievements.find(a => a.id === card.dataset.id); if (ach) openAchModal(ach); }); }); } function achCardHtml(a, idx) { return `
${escHtml(a.title)}
${escHtml(a.rank)}
${escHtml(a.category)} ${escHtml(String(a.year))}

${escHtml(a.title)}

${escHtml(a.description)}

${escHtml(a.avatar)}
${escHtml(a.team)} ${escHtml(a.generation)}
${escHtml(a.level)}
`; } function bindAchEvents() { const search = document.getElementById('acaab-ach-search'); const level = document.getElementById('acaab-ach-level'); const cat = document.getElementById('acaab-ach-cat'); const year = document.getElementById('acaab-ach-year'); const reset = document.getElementById('acaab-ach-reset'); const moreBtn = document.getElementById('acaab-ach-more'); const lessBtn = document.getElementById('acaab-ach-less'); const onChange = () => { achFilter.search = search ? search.value.trim() : ''; achFilter.level = level ? level.value : ''; achFilter.cat = cat ? cat.value : ''; achFilter.year = year ? year.value : ''; achVisible = ACH_INIT; renderAchievements(); }; if (search) search.addEventListener('input', onChange); if (level) level.addEventListener('change', onChange); if (cat) cat.addEventListener('change', onChange); if (year) year.addEventListener('change', onChange); if (reset) reset.addEventListener('click', () => { achFilter = { search: '', level: '', cat: '', year: '' }; if (search) search.value = ''; if (level) level.value = ''; if (cat) cat.value = ''; if (year) year.value = ''; achVisible = ACH_INIT; renderAchievements(); }); if (moreBtn) moreBtn.addEventListener('click', () => { achVisible += ACH_MORE; renderAchievements(); }); if (lessBtn) lessBtn.addEventListener('click', () => { achVisible = ACH_INIT; document.getElementById('acaab-ach-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); renderAchievements(); }); } // ─── Achievement Modal ──────────────────────────────────────────────────────── function infoRow(icon, label, value) { if (!value) return ''; return `

${escHtml(label)}

${escHtml(value)}

`; } function memberItem(nama, sub) { const initial = (nama || '?').charAt(0).toUpperCase(); return `
  • ${initial}
    ${escHtml(nama)} ${sub ? `${escHtml(sub)}` : ''}
  • `; } function openAchModal(a) { const membersHtml = a.members && a.members.length > 0 ? `

    Anggota Tim

    ` : ''; renderModal(`
    ${escHtml(a.title)}
    ${escHtml(a.rank)} ${escHtml(a.level)}
    ${escHtml(a.category)}

    ${escHtml(a.title)}

    ${infoRow('users', t('grp_acaab_modal_team'), a.team)} ${a.nim ? infoRow('id-card', 'NIM', a.nim) : ''} ${infoRow('user-graduate', t('grp_acaab_modal_generation'), a.generation)} ${infoRow('calendar-alt', 'Tahun', String(a.year))} ${infoRow('award', 'Kompetisi', a.competition)} ${infoRow('tag', 'Bidang', a.category)}
    ${membersHtml}

    Deskripsi

    ${escHtml(a.description)}

    `, true); } // ─── Projects ───────────────────────────────────────────────────────────────── const PROJ_INIT = 6; const PROJ_MORE = 3; function initProjects() { buildProjFilterOptions(); renderProjects(); bindProjEvents(); } function buildProjFilterOptions() { const catSel = document.getElementById('acaab-proj-cat'); const yearSel = document.getElementById('acaab-proj-year'); if (!catSel || !yearSel) return; const cats = [...new Set(projects.flatMap(p => p.categories))].sort(); const years = [...new Set(projects.map(p => String(p.year)))].filter(Boolean).sort((a, b) => b - a); catSel.innerHTML = `` + cats.map(c => ``).join(''); yearSel.innerHTML = `` + years.map(y => ``).join(''); } function filterProjects() { const q = projFilter.search.toLowerCase(); const result = projects.filter(p => { const matchSearch = !q || [p.title, p.studentName, p.description, ...p.categories] .some(s => (s || '').toLowerCase().includes(q)); const matchCat = !projFilter.cat || p.categories.includes(projFilter.cat); const matchYear = !projFilter.year || String(p.year) === projFilter.year; return matchSearch && matchCat && matchYear; }); // Sort descending by year result.sort((a, b) => (b.year || 0) - (a.year || 0)); return result; } function renderProjects() { const track = document.getElementById('acaab-proj-track'); const moreBtn = document.getElementById('acaab-proj-more'); const resetBtn = document.getElementById('acaab-proj-reset'); if (!track) return; const filtered = filterProjects(); const visible = filtered.slice(0, projVisible); const hasFilter = projFilter.search || projFilter.cat || projFilter.year; if (resetBtn) resetBtn.style.display = hasFilter ? 'flex' : 'none'; if (filtered.length === 0) { track.innerHTML = `

    ${escHtml(t('grp_acaab_proj_empty'))}

    `; if (moreBtn) moreBtn.style.display = 'none'; return; } track.innerHTML = visible.map((p, i) => projCardHtml(p, i)).join(''); if (moreBtn) moreBtn.style.display = projVisible < filtered.length ? 'inline-block' : 'none'; // Responsif: Sembunyikan scroll button & center jika cuma 1 data const prevBtn = document.getElementById('acaab-proj-prev'); const nextBtn = document.getElementById('acaab-proj-next'); if (prevBtn) prevBtn.style.display = filtered.length <= 1 ? 'none' : 'flex'; if (nextBtn) nextBtn.style.display = filtered.length <= 1 ? 'none' : 'flex'; track.style.justifyContent = filtered.length === 1 ? 'center' : 'flex-start'; track.querySelectorAll('.acaab-proj-card-wrap').forEach(card => { card.addEventListener('click', (e) => { if (e.target.closest('a') && e.target.closest('a').href) return; const proj = projects.find(p => p.id === card.dataset.id); if (proj) openProjModal(proj); }); }); } function projCardHtml(p, idx) { const catBadges = p.categories.map(c => ` ${escHtml(c)} `).join(''); return `
    ${escHtml(p.title)}

    ${escHtml(p.title)}

    ${catBadges}
    ${p.studentName ? `${escHtml(p.studentName)}` : ''} ${p.year ? `${escHtml(String(p.year))}` : ''}

    ${escHtml(p.description)}

    ${p.link ? ` ${escHtml(t('grp_acaab_modal_visit'))} ` : ''}
    `; } function bindProjEvents() { const search = document.getElementById('acaab-proj-search'); const cat = document.getElementById('acaab-proj-cat'); const year = document.getElementById('acaab-proj-year'); const reset = document.getElementById('acaab-proj-reset'); const moreBtn = document.getElementById('acaab-proj-more'); const prevBtn = document.getElementById('acaab-proj-prev'); const nextBtn = document.getElementById('acaab-proj-next'); const onChange = () => { projFilter.search = search ? search.value.trim() : ''; projFilter.cat = cat ? cat.value : ''; projFilter.year = year ? year.value : ''; projVisible = PROJ_INIT; renderProjects(); }; if (search) search.addEventListener('input', onChange); if (cat) cat.addEventListener('change', onChange); if (year) year.addEventListener('change', onChange); if (reset) reset.addEventListener('click', () => { projFilter = { search: '', cat: '', year: '' }; if (search) search.value = ''; if (cat) cat.value = ''; if (year) year.value = ''; projVisible = PROJ_INIT; renderProjects(); }); if (moreBtn) moreBtn.addEventListener('click', () => { projVisible += PROJ_MORE; renderProjects(); }); const track = document.getElementById('acaab-proj-track'); if (prevBtn && track) prevBtn.addEventListener('click', () => { const cardW = (track.firstElementChild ? track.firstElementChild.offsetWidth : 300) + 20; track.scrollBy({ left: -cardW, behavior: 'smooth' }); }); if (nextBtn && track) nextBtn.addEventListener('click', () => { const cardW = (track.firstElementChild ? track.firstElementChild.offsetWidth : 300) + 20; track.scrollBy({ left: cardW, behavior: 'smooth' }); }); } // ─── Project Modal ──────────────────────────────────────────────────────────── function openProjModal(p) { const membersHtml = p.members && p.members.length > 0 ? `

    Anggota Tim

    ` : ''; const catBadgesImg = p.categories.map(c => ` ${escHtml(c)} `).join(''); const linkSection = p.link ? ` ${escHtml(t('grp_acaab_modal_visit'))} ` : `
    ${escHtml(t('grp_acaab_modal_no_link'))}
    `; renderModal(`
    ${escHtml(p.title)}
    ${catBadgesImg}

    ${escHtml(p.title)}

    ${p.studentName ? infoRow('users', t('grp_acaab_modal_team'), p.studentName) : ''} ${p.nim ? infoRow('id-card', 'NIM', p.nim) : ''} ${infoRow('calendar-alt', 'Tahun', String(p.year))} ${infoRow('tag', 'Bidang', p.categories.join(', '))}
    ${membersHtml}

    Deskripsi

    ${escHtml(p.description)}

    ${p.catatan ? `

    Catatan Tambahan

    ${escHtml(p.catatan)}

    ` : ''}
    ${linkSection}
    `, true); } // ─── Modal shared ───────────────────────────────────────────────────────────── function renderModal(innerHtml, hasCloseBtn = false) { const container = document.getElementById('acaab-modal'); if (!container) return; container.innerHTML = `
    ${innerHtml}
    `; document.body.classList.add('acaab-modal-open'); document.getElementById('acaab-modal-close')?.addEventListener('click', closeModal); if (hasCloseBtn) { document.getElementById('acaab-modal-close-btn')?.addEventListener('click', closeModal); } document.getElementById('acaab-modal-bd').addEventListener('click', e => { if (e.target === e.currentTarget) closeModal(); }); document.addEventListener('keydown', onEsc); } function closeModal() { const c = document.getElementById('acaab-modal'); if (c) c.innerHTML = ''; document.body.classList.remove('acaab-modal-open'); document.removeEventListener('keydown', onEsc); } function onEsc(e) { if (e.key === 'Escape') closeModal(); } // ─── CTA / WhatsApp ─────────────────────────────────────────────────────────── const WA_NUMBER = '6285177655161'; function initWaButton() { updateWaButton(); } function updateWaButton() { const btn = document.getElementById('acaab-cta-wa'); if (!btn) return; const msg = getLang() === 'en' ? 'Hello! I would like to know more about Informatika UNTAN student achievements and projects.' : 'Halo! Saya ingin mengetahui lebih lanjut tentang prestasi dan karya mahasiswa Informatika UNTAN.'; btn.href = `https://wa.me/${WA_NUMBER}?text=${encodeURIComponent(msg)}`; } // ─── Placeholder sync ───────────────────────────────────────────────────────── function updateSearchPlaceholders() { const s1 = document.getElementById('acaab-ach-search'); const s2 = document.getElementById('acaab-proj-search'); if (s1) s1.placeholder = t('grp_acaab_ach_search'); if (s2) s2.placeholder = t('grp_acaab_proj_search'); }