Files
2026-06-05 13:19:40 +07:00

838 lines
44 KiB
JavaScript

/**
* 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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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 ? `${t('grp_acaab_modal_batch_prefix')} ${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(() =>
`<div class="acaab-skeleton" style="height:220px;border-radius:0.75rem;"></div>`
).join('');
}
function showError(containerId, msg) {
const el = document.getElementById(containerId);
if (el) el.innerHTML = `<p style="color:#ef4444;padding:1rem 0;grid-column:1/-1;">${escHtml(msg)}</p>`;
}
// ─── 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 = `<option value="">${escHtml(t('grp_acaab_ach_filter_level'))}</option>` +
levels.map(l => `<option value="${escHtml(l)}"${achFilter.level === l ? ' selected' : ''}>${escHtml(l)}</option>`).join('');
catSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_ach_filter_cat'))}</option>` +
cats.map(c => `<option value="${escHtml(c)}"${achFilter.cat === c ? ' selected' : ''}>${escHtml(c)}</option>`).join('');
yearSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_ach_filter_year'))}</option>` +
years.map(y => `<option value="${escHtml(y)}"${achFilter.year === y ? ' selected' : ''}>${escHtml(y)}</option>`).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 = `<p style="color:#64748b;padding:0.5rem 0;grid-column:1/-1;">${escHtml(t('grp_acaab_ach_empty'))}</p>`;
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 `
<article class="acaab-ach-card acaab-fade-in" data-id="${escHtml(a.id)}" style="animation-delay:${idx * 0.05}s;cursor:pointer;">
<div class="acaab-ach-top-bar"></div>
<!-- Image h-56 (224px) -->
<div style="position:relative;height:224px;overflow:hidden;background:#f1f5f9;">
<img src="${escHtml(a.image)}" alt="${escHtml(a.title)}" loading="lazy" class="acaab-card-img"
style="width:100%;height:100%;object-fit:cover;"
onerror="this.src='https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN'">
<!-- Rank badge top-left: white pill dengan trophy icon -->
<div style="position:absolute;top:0.75rem;left:0.75rem;background:rgba(255,255,255,0.95);backdrop-filter:blur(4px);color:#003150;font-weight:700;padding:0.25rem 0.625rem;border-radius:9999px;box-shadow:0 1px 3px rgba(0,0,0,0.15);display:flex;align-items:center;gap:0.375rem;font-size:0.72rem;z-index:2;">
<i class="fas fa-trophy" style="color:#feb401;font-size:0.65rem;"></i>
${escHtml(a.rank)}
</div>
</div>
<!-- Content p-6 -->
<div style="padding:1.5rem;display:flex;flex-direction:column;flex-grow:1;">
<!-- Category + Year row -->
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.75rem;">
<span style="color:#1e40af;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;">${escHtml(a.category)}</span>
<span style="color:#94a3b8;font-size:0.72rem;font-weight:500;display:flex;align-items:center;gap:0.25rem;border-left:1px solid #e2e8f0;padding-left:0.625rem;">
<i class="fas fa-calendar-alt" style="font-size:0.6rem;"></i> ${escHtml(String(a.year))}
</span>
</div>
<!-- Title -->
<h3 class="acaab-card-title acaab-line-clamp-2" style="font-size:1.125rem;font-weight:700;color:#003150;margin:0 0 0.75rem;line-height:1.35;font-family:'Roboto Condensed',sans-serif;">${escHtml(a.title)}</h3>
<!-- Description 3 lines -->
<p class="acaab-line-clamp-3" style="color:#475569;font-size:0.875rem;line-height:1.6;margin:0 0 1.5rem;flex-grow:1;">${escHtml(a.description)}</p>
<!-- Bottom -->
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-top:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<!-- Avatar + team + generation -->
<div style="display:flex;align-items:center;gap:0.625rem;">
<div style="width:2rem;height:2rem;border-radius:0.375rem;background:rgba(0,49,80,0.05);border:1px solid rgba(0,49,80,0.1);display:flex;align-items:center;justify-content:center;color:#003150;font-weight:700;font-size:0.65rem;flex-shrink:0;">${escHtml(a.avatar)}</div>
<div>
<span style="font-weight:700;color:#1e293b;display:block;font-size:0.8125rem;line-height:1.2;">${escHtml(a.team)}</span>
<span style="color:#64748b;display:block;font-size:0.72rem;margin-top:0.1rem;">${escHtml(a.generation)}</span>
</div>
</div>
<!-- Level badge -->
<span style="background:#f1f5f9;color:#475569;font-size:0.575rem;padding:0.2rem 0.5rem;border-radius:0.25rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;white-space:nowrap;">${escHtml(a.level)}</span>
</div>
<!-- Full-width CTA button -->
<button class="acaab-ach-readmore acaab-ach-read-btn" data-id="${escHtml(a.id)}"
style="width:100%;padding:0.625rem;border-radius:0.375rem;background:rgba(0,49,80,0.05);color:#003150;font-weight:700;font-size:0.875rem;border:none;cursor:pointer;transition:background 0.2s,color 0.2s;min-height:44px;">
${escHtml(t('grp_acaab_modal_read_more'))}
</button>
</div>
</div>
</article>`;
}
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 `<div style="display:flex;gap:0.625rem;align-items:flex-start;">
<span style="color:#feb401;margin-top:0.125rem;flex-shrink:0;"><i class="fas fa-${icon}" style="font-size:0.75rem;"></i></span>
<div>
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.125rem;line-height:1;">${escHtml(label)}</p>
<p style="font-size:0.875rem;font-weight:600;color:#1e293b;margin:0;line-height:1.3;">${escHtml(value)}</p>
</div>
</div>`;
}
function memberItem(nama, sub) {
const initial = (nama || '?').charAt(0).toUpperCase();
return `<li style="display:flex;align-items:center;gap:0.5rem;background:#f8fafc;padding:0.5rem 0.75rem;border-radius:0.375rem;">
<span style="width:1.5rem;height:1.5rem;border-radius:50%;background:rgba(254,180,1,0.2);color:#b45309;display:flex;align-items:center;justify-content:center;font-size:0.72rem;font-weight:700;flex-shrink:0;">${initial}</span>
<div style="display:flex;flex-direction:column;">
<span style="font-size:0.875rem;font-weight:600;color:#334155;line-height:1.2;">${escHtml(nama)}</span>
${sub ? `<span style="font-size:0.65rem;color:#94a3b8;margin-top:0.125rem;">${escHtml(sub)}</span>` : ''}
</div>
</li>`;
}
function openAchModal(a) {
const membersHtml = a.members && a.members.length > 0 ? `
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1rem;">
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_members')}</p>
<ul style="list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
${a.members.map(m => memberItem(m.nama, m.angkatan ? `${t('grp_acaab_modal_batch_prefix')} ${m.angkatan}` : '')).join('')}
</ul>
</div>` : '';
renderModal(`
<!-- Image header -->
<div style="position:relative;border-radius:0.75rem 0.75rem 0 0;background:#0f172a;flex-shrink:0;">
<img src="${escHtml(a.image)}" alt="${escHtml(a.title)}" style="width:100%;max-height:450px;object-fit:contain;display:block;"
onerror="this.src='https://placehold.co/800x600/0f172a/FDB813?text=Informatika+UNTAN'">
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.1) 40%,transparent 100%);pointer-events:none;"></div>
<!-- Close button: white rounded-full -->
<button id="acaab-modal-close" aria-label="Tutup"
style="position:absolute;top:0.75rem;right:0.75rem;width:2.25rem;height:2.25rem;background:rgba(255,255,255,0.9);backdrop-filter:blur(4px);border:none;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">
<i class="fas fa-times" style="color:#334155;font-size:0.875rem;"></i>
</button>
<!-- Two badges bottom-left -->
<div style="position:absolute;bottom:1rem;left:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
<span style="background:#feb401;color:#0f172a;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;display:flex;align-items:center;gap:0.375rem;">
<i class="fas fa-trophy" style="font-size:0.65rem;"></i>${escHtml(a.rank)}
</span>
<span style="background:rgba(255,255,255,0.2);backdrop-filter:blur(4px);color:white;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;text-transform:uppercase;letter-spacing:0.05em;">
${escHtml(a.level)}
</span>
</div>
</div>
<!-- Content -->
<div style="padding:1.5rem;">
<span style="color:#1e40af;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;">${escHtml(a.category)}</span>
<h2 style="font-size:1.125rem;font-weight:700;color:#003150;line-height:1.3;margin:0.25rem 0 1.25rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(a.title)}</h2>
<!-- Info grid: slate-50 bg -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;padding:1rem;background:#f8fafc;border-radius:0.5rem;margin-bottom:1.25rem;">
${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', t('grp_acaab_modal_year'), String(a.year))}
${infoRow('award', t('grp_acaab_modal_competition'), a.competition)}
${infoRow('tag', t('grp_acaab_modal_field'), a.category)}
</div>
${membersHtml}
<!-- Description -->
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;">
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_description')}</p>
<p style="color:#475569;font-size:0.875rem;line-height:1.65;margin:0;">${escHtml(a.description)}</p>
</div>
<!-- Close button full-width -->
<button id="acaab-modal-close-btn" style="margin-top:1.5rem;width:100%;padding:0.75rem;background:#003150;color:white;font-weight:700;border:none;border-radius:0.5rem;cursor:pointer;font-size:0.875rem;">
${t('grp_acaab_modal_close')}
</button>
</div>
`, 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 = `<option value="">${escHtml(t('grp_acaab_proj_filter_cat'))}</option>` +
cats.map(c => `<option value="${escHtml(c)}"${projFilter.cat === c ? ' selected' : ''}>${escHtml(c)}</option>`).join('');
yearSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_proj_filter_year'))}</option>` +
years.map(y => `<option value="${escHtml(y)}"${projFilter.year === y ? ' selected' : ''}>${escHtml(y)}</option>`).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 = `<p style="color:#64748b;padding:0.5rem 0;">${escHtml(t('grp_acaab_proj_empty'))}</p>`;
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 => `
<span style="display:inline-flex;align-items:center;gap:0.2rem;font-size:0.6rem;font-weight:700;color:#feb401;text-transform:uppercase;letter-spacing:0.05em;background:rgba(0,0,0,0.3);padding:0.2rem 0.45rem;border-radius:0.2rem;backdrop-filter:blur(4px);">
<i class="fas fa-tag" style="font-size:0.5rem;"></i>${escHtml(c)}
</span>`).join('');
return `
<div class="acaab-proj-card acaab-proj-card-wrap acaab-fade-in" data-id="${escHtml(p.id)}" style="animation-delay:${idx * 0.05}s;cursor:pointer;">
<div class="acaab-proj-left-bar"></div>
<!-- Image h-52 (208px) dengan title overlay -->
<div style="height:208px;overflow:hidden;position:relative;">
<img src="${escHtml(p.image)}" alt="${escHtml(p.title)}" loading="lazy" class="acaab-card-img"
style="width:100%;height:100%;object-fit:cover;"
onerror="this.src='https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN'">
<!-- Gradient overlay: title + categories di dalam gambar -->
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,49,80,0.85),transparent);padding:1.25rem;display:flex;flex-direction:column;justify-content:flex-end;pointer-events:none;">
<h4 style="font-size:1rem;font-weight:700;color:white;line-height:1.25;margin:0 0 0.375rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(p.title)}</h4>
<div style="display:flex;flex-wrap:wrap;gap:0.3rem;">${catBadges}</div>
</div>
</div>
<!-- Body p-5 -->
<div style="padding:1.25rem;display:flex;flex-direction:column;flex-grow:1;">
<!-- Meta: student + year -->
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.625rem;margin-bottom:0.75rem;font-size:0.72rem;color:#64748b;font-weight:500;">
${p.studentName ? `<span style="display:flex;align-items:center;gap:0.25rem;"><i class="fas fa-user" style="color:#94a3b8;font-size:0.6rem;"></i>${escHtml(p.studentName)}</span>` : ''}
${p.year ? `<span style="display:flex;align-items:center;gap:0.25rem;"><i class="fas fa-calendar-alt" style="color:#94a3b8;font-size:0.6rem;"></i>${escHtml(String(p.year))}</span>` : ''}
</div>
<!-- Description 3 lines -->
<p class="acaab-line-clamp-3" style="color:#475569;font-size:0.875rem;line-height:1.6;margin:0 0 1.25rem;flex-grow:1;">${escHtml(p.description)}</p>
<!-- Actions -->
<div style="display:flex;align-items:center;gap:0.625rem;margin-top:auto;">
<button class="acaab-proj-readmore" data-id="${escHtml(p.id)}"
style="color:#003150;font-weight:700;font-size:0.875rem;background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:0.375rem;min-height:44px;">
${escHtml(t('grp_acaab_modal_read_more'))} <i class="fas fa-arrow-right" style="font-size:0.7rem;"></i>
</button>
${p.link ? `
<a href="${escHtml(p.link)}" target="_blank" rel="noopener noreferrer"
style="margin-left:auto;display:flex;align-items:center;gap:0.375rem;font-size:0.72rem;font-weight:700;color:#64748b;border:1px solid #e2e8f0;border-radius:0.375rem;padding:0.375rem 0.625rem;text-decoration:none;min-height:44px;transition:color 0.2s,border-color 0.2s;">
<i class="fas fa-external-link-alt" style="font-size:0.65rem;"></i>${escHtml(t('grp_acaab_modal_visit'))}
</a>` : ''}
</div>
</div>
</div>`;
}
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 ? `
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1rem;">
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_members')}</p>
<ul style="list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
${p.members.map(m => memberItem(m.nama, m.nim || '')).join('')}
</ul>
</div>` : '';
const catBadgesImg = p.categories.map(c => `
<span style="background:#feb401;color:#0f172a;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;display:inline-flex;align-items:center;gap:0.375rem;text-transform:uppercase;letter-spacing:0.05em;">
<i class="fas fa-tag" style="font-size:0.6rem;"></i>${escHtml(c)}
</span>`).join('');
const linkSection = p.link
? `<a href="${escHtml(p.link)}" target="_blank" rel="noopener noreferrer"
style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;padding:0.75rem;background:#003150;color:white;font-weight:700;border-radius:0.5rem;font-size:0.875rem;text-decoration:none;margin-bottom:0.625rem;">
<i class="fas fa-external-link-alt" style="font-size:0.8rem;"></i>${escHtml(t('grp_acaab_modal_visit'))}
</a>`
: `<div style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;padding:0.75rem;background:#f1f5f9;color:#94a3b8;font-weight:700;border-radius:0.5rem;font-size:0.875rem;margin-bottom:0.625rem;cursor:not-allowed;">
<i class="fas fa-file-alt" style="font-size:0.8rem;"></i>${escHtml(t('grp_acaab_modal_no_link'))}
</div>`;
renderModal(`
<!-- Image header -->
<div style="position:relative;border-radius:0.75rem 0.75rem 0 0;background:#0f172a;flex-shrink:0;">
<img src="${escHtml(p.image)}" alt="${escHtml(p.title)}" style="width:100%;max-height:450px;object-fit:contain;display:block;"
onerror="this.src='https://placehold.co/800x600/0f172a/FDB813?text=Informatika+UNTAN'">
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.1) 40%,transparent 100%);pointer-events:none;"></div>
<!-- Close button -->
<button id="acaab-modal-close" aria-label="Tutup"
style="position:absolute;top:0.75rem;right:0.75rem;width:2.25rem;height:2.25rem;background:rgba(255,255,255,0.9);backdrop-filter:blur(4px);border:none;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">
<i class="fas fa-times" style="color:#334155;font-size:0.875rem;"></i>
</button>
<!-- Category badges bottom-left -->
<div style="position:absolute;bottom:1rem;left:1rem;display:flex;flex-wrap:wrap;gap:0.375rem;">${catBadgesImg}</div>
</div>
<!-- Content -->
<div style="padding:1.5rem;">
<h2 style="font-size:1.125rem;font-weight:700;color:#003150;line-height:1.3;margin:0 0 1.25rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(p.title)}</h2>
<!-- Info grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;padding:1rem;background:#f8fafc;border-radius:0.5rem;margin-bottom:1.25rem;">
${p.studentName ? infoRow('users', t('grp_acaab_modal_team'), p.studentName) : ''}
${p.nim ? infoRow('id-card', 'NIM', p.nim) : ''}
${infoRow('calendar-alt', t('grp_acaab_modal_year'), String(p.year))}
${infoRow('tag', t('grp_acaab_modal_field'), p.categories.join(', '))}
</div>
${membersHtml}
<!-- Description -->
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1.25rem;">
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_description')}</p>
<p style="color:#475569;font-size:0.875rem;line-height:1.65;margin:0;">${escHtml(p.description)}</p>
${p.catatan ? `
<div style="margin-top:1rem;padding:0.75rem;background:rgba(239,246,255,0.5);border:1px solid #bfdbfe;border-radius:0.5rem;">
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;color:#93c5fd;margin:0 0 0.25rem;">${t('grp_acaab_modal_note')}</p>
<p style="font-size:0.875rem;color:#475569;font-style:italic;margin:0;">${escHtml(p.catatan)}</p>
</div>` : ''}
</div>
<!-- Link + close buttons -->
${linkSection}
<button id="acaab-modal-close-btn" style="width:100%;padding:0.625rem;border:2px solid #e2e8f0;color:#475569;font-weight:700;background:white;border-radius:0.5rem;cursor:pointer;font-size:0.875rem;transition:border-color 0.2s,background 0.2s;">
${t('grp_acaab_modal_close')}
</button>
</div>
`, true);
}
// ─── Modal shared ─────────────────────────────────────────────────────────────
function renderModal(innerHtml, hasCloseBtn = false) {
const container = document.getElementById('acaab-modal');
if (!container) return;
container.innerHTML = `
<div class="acaab-modal-backdrop" id="acaab-modal-bd">
<div style="background:white;border-radius:0.75rem;max-width:672px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.35);">
${innerHtml}
</div>
</div>`;
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');
}