@@ -2758,6 +2779,98 @@
}
}
+ /* ── SKILL DISTRIBUTION — Live fetch from Google Sheets (approved rows only) ── */
+ (function fetchSkillData() {
+ const SHEET_ID = '1mtVB1vLev-OWz6aTuuddGKPS8tCv8MJcGpzws8irpxk';
+ // Col I=Skill1, J=Skill2, K=Skill3, L=Approved (admin status)
+ const query = encodeURIComponent("select I,J,K where lower(L)='approved'");
+ const url = `https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?tqx=out:json&tq=${query}`;
+
+ // Map: skill label → bar id prefix (must match id="bar-XXX" and id="pct-XXX" in HTML)
+ const SKILL_MAP = {
+ 'Web Development' : 'web-development',
+ 'Mobile Development' : 'mobile-development',
+ 'Python / Data Science': 'python-data-science',
+ 'UI/UX Design' : 'uiux-design',
+ 'Networking / DevOps' : 'networking-devops',
+ 'Cybersecurity' : 'cybersecurity',
+ 'Machine Learning / AI': 'machine-learning-ai',
+ 'Database' : 'database',
+ };
+
+ const spinner = document.getElementById('skill-note-spinner');
+ const noteText = document.getElementById('skill-note-text');
+
+ function setNote(text, spin, isCta) {
+ if (spinner) spinner.style.animation = spin ? 'spin 1s linear infinite' : 'none';
+ if (!noteText) return;
+ if (isCta) {
+ noteText.innerHTML = text + '
Isi form kontribusi';
+ } else {
+ noteText.textContent = text;
+ }
+ }
+
+ function normalizeSkill(val) {
+ if (!val) return null;
+ const s = val.toLowerCase().trim();
+ if (s.includes('web')) return 'Web Development';
+ if (s.includes('mobile')) return 'Mobile Development';
+ if (s.includes('python') || s.includes('data science') || s.includes('data science')) return 'Python / Data Science';
+ if (s.includes('ui') || s.includes('ux')) return 'UI/UX Design';
+ if (s.includes('network') || s.includes('devops')) return 'Networking / DevOps';
+ if (s.includes('cyber')) return 'Cybersecurity';
+ if (s.includes('machine') || s.includes('ai') || s.includes('artificial')) return 'Machine Learning / AI';
+ if (s.includes('database') || s.includes('db')) return 'Database';
+ return null;
+ }
+
+ fetch(url)
+ .then(res => res.text())
+ .then(raw => {
+ const json = JSON.parse(raw.replace(/^[^{]+/, '').replace(/[);]+$/, ''));
+ const rows = (json.table && json.table.rows) ? json.table.rows : [];
+
+ if (rows.length === 0) {
+ setNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false);
+ return;
+ }
+
+ // Count normalized skill mentions
+ const counts = {};
+ rows.forEach(row => {
+ [0, 1, 2].forEach(colIdx => {
+ const cell = row.c && row.c[colIdx];
+ const val = cell && cell.v ? String(cell.v).trim() : '';
+ const normalized = normalizeSkill(val);
+ if (normalized) {
+ counts[normalized] = (counts[normalized] || 0) + 1;
+ }
+ });
+ });
+
+ const totalMentions = Object.values(counts).reduce((s, c) => s + c, 0);
+ const maxCount = Math.max(...Object.values(counts), 0);
+
+ // Update bars for predefined skills
+ Object.entries(SKILL_MAP).forEach(([label, idKey]) => {
+ const count = counts[label] || 0;
+ const pct = totalMentions > 0 ? Math.round((count / totalMentions) * 100) : 0;
+ const barPct = maxCount > 0 ? Math.round((count / maxCount) * 100) : 0;
+
+ const barEl = document.getElementById('bar-' + idKey);
+ const pctEl = document.getElementById('pct-' + idKey);
+ if (barEl) barEl.style.width = barPct + '%';
+ if (pctEl) pctEl.textContent = count > 0 ? pct + '%' : '—';
+ });
+
+ setNote(`Data dari ${rows.length} alumni yang diverifikasi.`, false, false);
+ })
+ .catch(() => {
+ setNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false);
+ });
+ })();
+
/* ── REVEAL ON SCROLL ── */
const ro = new IntersectionObserver(
(entries) => {