forked from izu/student-web-if-development-kit
833 lines
43 KiB
JavaScript
833 lines
43 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, '<')
|
|
.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(() =>
|
|
`<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;">Anggota Tim</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 ? `Angkatan ${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', 'Tahun', String(a.year))}
|
|
${infoRow('award', 'Kompetisi', a.competition)}
|
|
${infoRow('tag', 'Bidang', 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;">Deskripsi</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;">
|
|
Tutup
|
|
</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();
|
|
return 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;
|
|
});
|
|
}
|
|
|
|
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;">Anggota Tim</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', 'Tahun', String(p.year))}
|
|
${infoRow('tag', 'Bidang', 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;">Deskripsi</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;">Catatan Tambahan</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;">
|
|
Tutup
|
|
</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');
|
|
}
|