10 Commits

Author SHA1 Message Date
Nayyara_04 a09ddac2c6 Merge pull request 'fix(user): tambah mobile responsive padding dan perataan baris skill' (#6) from feat/responsive-layout into development
Reviewed-on: #6
2026-06-05 02:31:01 +00:00
Mhanif26 bc173ff4ec fix(m4n1fest): tambah mobile responsive padding dan perataan baris skil 2026-06-05 09:26:44 +07:00
Nayyara_04 b844ca8b23 Merge pull request 'feat(m4n1fest): menerapkan clean code pada kode dan tambah komen untuk memudahkan pembacaan' (#4) from feat/clean-code into development
Reviewed-on: #4
2026-06-05 01:47:47 +00:00
Mhanif26 471ed6cd18 feat(m4n1fest): menerapkan clean code pada kode dan tambah komen untuk memudahkan pembacaan 2026-06-05 08:36:26 +07:00
Nayyara_04 874c185dc9 Merge pull request 'fix(m4n1fest): perbaikan jumlah alumni di hero dan swap table-chart layout' (#3) from fix/m4n1fest-stats-layout into development
Reviewed-on: #3
2026-06-05 01:25:17 +00:00
Mhanif26 fe5c645cbd chore: resolve merge conflict in index.html 2026-06-05 08:10:06 +07:00
Mhanif26 ec0288fd8a fix(m4n1fest): perbaikan jumlah alumni di hero dan swap table-chart layout 2026-06-05 07:48:56 +07:00
Nayyara_04 7cf671d826 Merge pull request 'feat: menambahkan logo perusahaan dan gambar dokumentasi alumni high achievers' (#2) from feature/add-assets-high-achievers into development
Reviewed-on: #2
2026-06-04 21:09:19 +00:00
Mhanif26 c772aa2d83 feat: menambahkan logo perusahaan dan gambar dokumentasi alumni high achievers 2026-06-04 21:28:48 +07:00
Nayyara_04 f75c04c052 Merge pull request 'feat(alumni): integrasi data dinamis skill alumni dari google sheets' (#1) from feature/integrasi-data-skill into development
Reviewed-on: #1
2026-06-04 13:57:10 +00:00
19 changed files with 408 additions and 311 deletions
+2
View File
@@ -578,6 +578,7 @@ const translations = {
grp_m4n1fest_hero_btn_stats: "Lihat Statistik Alumni", grp_m4n1fest_hero_btn_stats: "Lihat Statistik Alumni",
grp_m4n1fest_hero_btn_join: "Gabung Grup IKA", grp_m4n1fest_hero_btn_join: "Gabung Grup IKA",
grp_m4n1fest_hero_stat_alumni: "TOTAL ALUMNI", grp_m4n1fest_hero_stat_alumni: "TOTAL ALUMNI",
grp_m4n1fest_hero_stat_alumni_desc: "data diperoleh dari alumni terdata di tracer study",
grp_m4n1fest_hero_stat_batch: "ANGKATAN", grp_m4n1fest_hero_stat_batch: "ANGKATAN",
grp_m4n1fest_hero_stat_company: "PERUSAHAAN", grp_m4n1fest_hero_stat_company: "PERUSAHAAN",
grp_m4n1fest_eyebrow_achievers: "High Achievers", grp_m4n1fest_eyebrow_achievers: "High Achievers",
@@ -1285,6 +1286,7 @@ const translations = {
grp_m4n1fest_hero_btn_stats: "View Alumni Statistics", grp_m4n1fest_hero_btn_stats: "View Alumni Statistics",
grp_m4n1fest_hero_btn_join: "Join Alumni Group", grp_m4n1fest_hero_btn_join: "Join Alumni Group",
grp_m4n1fest_hero_stat_alumni: "TOTAL ALUMNI", grp_m4n1fest_hero_stat_alumni: "TOTAL ALUMNI",
grp_m4n1fest_hero_stat_alumni_desc: "data obtained from alumni recorded in tracer study",
grp_m4n1fest_hero_stat_batch: "CLASSES", grp_m4n1fest_hero_stat_batch: "CLASSES",
grp_m4n1fest_hero_stat_company: "COMPANIES", grp_m4n1fest_hero_stat_company: "COMPANIES",
grp_m4n1fest_eyebrow_achievers: "High Achievers", grp_m4n1fest_eyebrow_achievers: "High Achievers",
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

+375 -310
View File
@@ -1188,15 +1188,16 @@
<div class="custom-stats"> <div class="custom-stats">
<div class="custom-stat-box"> <div class="custom-stat-box">
<div class="custom-stat-num custom-serif">850<span class="custom-stat-plus">+</span></div> <div class="custom-stat-num custom-serif">212<span class="custom-stat-plus">+</span></div>
<div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_alumni">TOTAL ALUMNI</div> <div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_alumni">TOTAL ALUMNI</div>
<div class="custom-stat-desc" style="font-size: 0.65rem; color: #a1a1aa; margin-top: 4px; line-height: 1.35; font-weight: normal; font-family: sans-serif;" data-i18n="grp_m4n1fest_hero_stat_alumni_desc">data diperoleh dari alumni terdata di tracer study</div>
</div> </div>
<div class="custom-stat-box"> <div class="custom-stat-box">
<div class="custom-stat-num custom-serif">18<span class="custom-stat-plus">+</span></div> <div class="custom-stat-num custom-serif">22</div>
<div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_batch">ANGKATAN</div> <div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_batch">ANGKATAN</div>
</div> </div>
<div class="custom-stat-box" style="border-right: none;"> <div class="custom-stat-box" style="border-right: none;">
<div class="custom-stat-num custom-serif">150<span class="custom-stat-plus">+</span></div> <div class="custom-stat-num custom-serif">202<span class="custom-stat-plus">+</span></div>
<div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_company">PERUSAHAAN</div> <div class="custom-stat-label" data-i18n="grp_m4n1fest_hero_stat_company">PERUSAHAAN</div>
</div> </div>
</div> </div>
@@ -1226,8 +1227,8 @@
<!-- Card 1 --> <!-- Card 1 -->
<div class="achiever-card rv d1"> <div class="achiever-card rv d1">
<div class="achiever-body"> <div class="achiever-body">
<div class="achiever-avatar-circle" style="background: linear-gradient(135deg, #1a3a6b, #2352a0);"> <div class="achiever-avatar-circle">
<span class="achiever-initials">IF</span> <img src="./assets/izhan.jpg" alt="Izhan Fakhruzi">
</div> </div>
<div class="achiever-batch" data-i18n="grp_m4n1fest_ach1_batch">Angkatan 2005</div> <div class="achiever-batch" data-i18n="grp_m4n1fest_ach1_batch">Angkatan 2005</div>
<div class="achiever-name">Izhan Fakhruzi</div> <div class="achiever-name">Izhan Fakhruzi</div>
@@ -1244,8 +1245,8 @@
<!-- Card 2 --> <!-- Card 2 -->
<div class="achiever-card rv d2"> <div class="achiever-card rv d2">
<div class="achiever-body"> <div class="achiever-body">
<div class="achiever-avatar-circle" style="background: linear-gradient(135deg, #0f766e, #14b8a6);"> <div class="achiever-avatar-circle">
<span class="achiever-initials">CD</span> <img src="./assets/charles.jpg" alt="Charles Darwis Hariman">
</div> </div>
<div class="achiever-batch" data-i18n="grp_m4n1fest_ach2_batch">Informatika UNTAN</div> <div class="achiever-batch" data-i18n="grp_m4n1fest_ach2_batch">Informatika UNTAN</div>
<div class="achiever-name">Charles Darwis Hariman</div> <div class="achiever-name">Charles Darwis Hariman</div>
@@ -1262,8 +1263,8 @@
<!-- Card 3 --> <!-- Card 3 -->
<div class="achiever-card rv d3"> <div class="achiever-card rv d3">
<div class="achiever-body"> <div class="achiever-body">
<div class="achiever-avatar-circle" style="background: linear-gradient(135deg, #3730a3, #6366f1);"> <div class="achiever-avatar-circle">
<span class="achiever-initials">DP</span> <img src="./assets/dona.jpg" alt="Dona Pramana Pura">
</div> </div>
<div class="achiever-batch" data-i18n="grp_m4n1fest_ach3_batch">Angkatan 2009</div> <div class="achiever-batch" data-i18n="grp_m4n1fest_ach3_batch">Angkatan 2009</div>
<div class="achiever-name">Dona Pramana Pura</div> <div class="achiever-name">Dona Pramana Pura</div>
@@ -1396,102 +1397,47 @@
</div> </div>
<div class="logo-grid"> <div class="logo-grid">
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/BCA.png" alt="Logo BCA" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #ebf1fb; color: #1a3a6b"
>
BCA
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_bca">BCA</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_bca">BCA</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/bri.png" alt="Logo BRI" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #fff0f0; color: #c00"
>
BRI
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_bri">BRI</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_bri">BRI</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/Telkom.jpg" alt="Logo Telkom Indonesia" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #fff7e6; color: #f5b800"
>
TLKM
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_telkom">Telkom Indonesia</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_telkom">Telkom Indonesia</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/pln.jpg" alt="Logo PLN" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #ebf1fb; color: #0a66c2"
>
PLN
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_pln">PLN</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_pln">PLN</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/Indofood.png" alt="Logo Indofood Fortuna" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #f0f0ff; color: #3730a3"
>
INDF
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_indofood">Indofood Fortuna</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_indofood">Indofood Fortuna</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/astra.webp" alt="Logo Astra Daihatsu" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #fff0f5; color: #ee4d2d"
>
ASTR
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_astra">Astra Daihatsu</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_astra">Astra Daihatsu</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/unilever-logo.png" alt="Logo Unilever" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #f0fff4; color: #15803d"
>
UNVR
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_unilever">Unilever</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_unilever">Unilever</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/grab.png" alt="Logo Grab" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #f0fff4; color: #16a34a"
>
GRAB
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_grab">Grab</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_grab">Grab</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/telkomsel.jpg" alt="Logo Telkomsel" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #fff0f0; color: #c00"
>
TSEL
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_telkomsel">Telkomsel</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_telkomsel">Telkomsel</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/garuda_indonesia.jpg" alt="Logo Garuda Indonesia" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #ebf1fb; color: #0866ff"
>
GIAA
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_garuda">Garuda Indonesia</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_garuda">Garuda Indonesia</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
<div <img src="./assets/Bulog-Logo.png" alt="Logo Perum Bulog" style="width: 100%; height: 60px; padding: 8px; object-fit: contain; border-radius: 8px;">
class="logo-initials"
style="background: #fffbea; color: #b38500"
>
BULG
</div>
<span class="logo-name" data-i18n="grp_m4n1fest_logo_bulog">Perum BULOG</span> <span class="logo-name" data-i18n="grp_m4n1fest_logo_bulog">Perum BULOG</span>
</div> </div>
<div class="logo-card"> <div class="logo-card">
@@ -1623,12 +1569,24 @@
</p> </p>
</div> </div>
<div class="two-col" style="align-items: flex-start;"> <div class="two-col stat-cols" style="align-items: flex-start;">
<!-- left: Table --> <!-- left: Chart -->
<div class="chart-box rv d2" style="margin-top: 0;">
<div class="chart-header">
<div class="chart-title" data-i18n="grp_m4n1fest_chart_title">
Grafik Jumlah Lulusan
</div>
</div>
<div style="position: relative; height: 280px; width: 100%;">
<canvas id="kelulusanChart"></canvas>
</div>
</div>
<!-- right: Table -->
<div class="rv d1" style="background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; padding: 0;"> <div class="rv d1" style="background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; padding: 0;">
<table style="width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem;"> <table style="width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem;">
<thead> <thead>
<tr style="background: #374151; color: #fff;"> <tr style="background: #1a3a6b; color: #fff;">
<th style="padding: 14px 20px; font-weight: 600;" data-i18n="grp_m4n1fest_table_head_period">Periode</th> <th style="padding: 14px 20px; font-weight: 600;" data-i18n="grp_m4n1fest_table_head_period">Periode</th>
<th style="padding: 14px 20px; font-weight: 600; text-align: right;" data-i18n="grp_m4n1fest_table_head_amount">Jumlah</th> <th style="padding: 14px 20px; font-weight: 600; text-align: right;" data-i18n="grp_m4n1fest_table_head_amount">Jumlah</th>
</tr> </tr>
@@ -1660,18 +1618,6 @@
Sumber : BAK UNTAN Sumber : BAK UNTAN
</div> </div>
</div> </div>
<!-- right: Chart -->
<div class="chart-box rv d2" style="margin-top: 0;">
<div class="chart-header">
<div class="chart-title" data-i18n="grp_m4n1fest_chart_title">
Grafik Jumlah Lulusan
</div>
</div>
<div style="position: relative; height: 280px; width: 100%;">
<canvas id="kelulusanChart"></canvas>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -2586,13 +2532,15 @@
</footer> </footer>
<script> <script>
// Manajemen Mega Menu Navigasi Desktop
(function () { (function () {
const menuItems = document.querySelectorAll('[data-menu]'); const menuItems = document.querySelectorAll('[data-menu]');
const megaOverlay = document.getElementById('mega-overlay'); const megaOverlay = document.getElementById('mega-overlay');
let activeMenu = null; let activeMenuId = null; // Menyimpan ID menu yang sedang aktif/terbuka
const menuMap = { // Pemetaan menu ke panel konten mega menu
const menuPanelMap = {
tentang: 'mega-tentang', tentang: 'mega-tentang',
program: 'mega-program', program: 'mega-program',
sdm: 'mega-sdm', sdm: 'mega-sdm',
@@ -2601,98 +2549,127 @@
kontak: 'mega-kontak' kontak: 'mega-kontak'
}; };
function setButtonState(menuId) { // Mengubah status visual tombol navigasi berdasarkan menu aktif
menuItems.forEach(item => { function setMenuButtonState(menuId) {
const isActive = item.getAttribute('data-menu') === menuId; menuItems.forEach(menuItemElement => {
const btn = item.querySelector('.nav-menu-btn'); const isActive = menuItemElement.getAttribute('data-menu') === menuId;
const chevron = item.querySelector('.fa-chevron-down'); const buttonElement = menuItemElement.querySelector('.nav-menu-btn');
const chevronIconElement = menuItemElement.querySelector('.fa-chevron-down');
btn.classList.toggle('active', isActive); if (buttonElement) {
btn.setAttribute('aria-expanded', isActive ? 'true' : 'false'); buttonElement.classList.toggle('active', isActive);
chevron.style.transform = isActive ? 'rotate(180deg)' : ''; buttonElement.setAttribute('aria-expanded', isActive ? 'true' : 'false');
}
if (chevronIconElement) {
chevronIconElement.style.transform = isActive ? 'rotate(180deg)' : '';
}
}); });
} }
function closeMenus() { // Menutup semua panel mega menu yang sedang terbuka
Object.values(menuMap).forEach(panelId => { function closeActiveMenus() {
const panel = document.getElementById(panelId); Object.values(menuPanelMap).forEach(panelId => {
if (panel) panel.classList.remove('active'); const panelElement = document.getElementById(panelId);
if (panelElement) {
panelElement.classList.remove('active');
}
}); });
megaOverlay.classList.remove('active'); if (megaOverlay) {
setButtonState(null); megaOverlay.classList.remove('active');
activeMenu = null; }
setMenuButtonState(null);
activeMenuId = null;
} }
function openMenu(menuId) { // Membuka panel mega menu tertentu dan menutup yang lainnya
const targetPanel = document.getElementById(menuMap[menuId]); function openSelectedMenu(menuId) {
if (!targetPanel) return; const targetPanelElement = document.getElementById(menuPanelMap[menuId]);
if (!targetPanelElement) return;
Object.values(menuMap).forEach(panelId => { Object.values(menuPanelMap).forEach(panelId => {
const panel = document.getElementById(panelId); const panelElement = document.getElementById(panelId);
if (panel) panel.classList.remove('active'); if (panelElement) {
panelElement.classList.remove('active');
}
}); });
targetPanel.classList.add('active'); targetPanelElement.classList.add('active');
megaOverlay.classList.add('active'); if (megaOverlay) {
setButtonState(menuId); megaOverlay.classList.add('active');
activeMenu = menuId; }
setMenuButtonState(menuId);
activeMenuId = menuId;
} }
menuItems.forEach(item => { // Menginisialisasi event listener untuk setiap tombol navigasi
const menuId = item.getAttribute('data-menu'); menuItems.forEach(menuItemElement => {
const button = item.querySelector('.nav-menu-btn'); const menuId = menuItemElement.getAttribute('data-menu');
const buttonElement = menuItemElement.querySelector('.nav-menu-btn');
if (!buttonElement) return;
button.setAttribute('aria-haspopup', 'true'); buttonElement.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', 'false'); buttonElement.setAttribute('aria-expanded', 'false');
button.addEventListener('click', (e) => { buttonElement.addEventListener('click', (event) => {
e.preventDefault(); event.preventDefault();
e.stopPropagation(); event.stopPropagation();
if (activeMenu === menuId) { if (activeMenuId === menuId) {
closeMenus(); closeActiveMenus();
} else { } else {
openMenu(menuId); openSelectedMenu(menuId);
} }
}); });
}); });
megaOverlay.addEventListener('click', closeMenus); // Menutup menu ketika overlay (latar belakang redup) diklik
if (megaOverlay) {
megaOverlay.addEventListener('click', closeActiveMenus);
}
document.addEventListener('click', (e) => { // Menutup menu jika klik dilakukan di luar area navigasi dan panel menu
const clickedInsideNav = e.target.closest('#main-nav'); document.addEventListener('click', (event) => {
const clickedInsidePanel = e.target.closest('.mega-panel'); const isClickInsideNav = event.target.closest('#main-nav');
const isClickInsidePanel = event.target.closest('.mega-panel');
if (!clickedInsideNav && !clickedInsidePanel && activeMenu) { if (!isClickInsideNav && !isClickInsidePanel && activeMenuId) {
closeMenus(); closeActiveMenus();
} }
}); });
document.addEventListener('keydown', (e) => { // Menutup menu jika menekan tombol Escape keyboard
if (e.key === 'Escape' && activeMenu) { document.addEventListener('keydown', (event) => {
closeMenus(); if (event.key === 'Escape' && activeMenuId) {
closeActiveMenus();
} }
}); });
})(); })();
</script> </script>
<script> <script>
// Manajemen Menu Navigasi Seluler (Mobile Menu)
(function() { (function() {
const mobileBtn = document.getElementById('mobile-menu-button'); const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu'); const mobileMenuPanel = document.getElementById('mobile-menu');
const closeMobileBtn= document.getElementById('close-mobile-menu'); const closeMobileMenuButton = document.getElementById('close-mobile-menu');
if (mobileBtn && mobileMenu) {
mobileBtn.addEventListener('click', function() { // Menampilkan atau menyembunyikan menu seluler saat tombol menu diklik
const isOpen = !mobileMenu.classList.contains('-translate-x-full'); if (mobileMenuButton && mobileMenuPanel) {
mobileMenu.classList.toggle('-translate-x-full'); mobileMenuButton.addEventListener('click', () => {
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true'); const isHidden = mobileMenuPanel.classList.contains('-translate-x-full');
mobileMenuPanel.classList.toggle('-translate-x-full');
mobileMenuButton.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
}); });
} }
if (closeMobileBtn && mobileMenu) {
closeMobileBtn.addEventListener('click', function() { // Menyembunyikan kembali menu seluler saat tombol silang (tutup) diklik
mobileMenu.classList.add('-translate-x-full'); if (closeMobileMenuButton && mobileMenuPanel) {
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false'); closeMobileMenuButton.addEventListener('click', () => {
mobileMenuPanel.classList.add('-translate-x-full');
if (mobileMenuButton) {
mobileMenuButton.setAttribute('aria-expanded', 'false');
}
}); });
} }
})(); })();
@@ -2700,217 +2677,305 @@
<!-- Scroll Reveal Observer & Chart.js Initialization --> <!-- Scroll Reveal Observer & Chart.js Initialization -->
<script> <script>
/* ── IKA FORM SUBMIT ── */ // URL Web App Google Apps Script untuk penanganan pengiriman form IKA
const IKA_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbwrXG3YFu4PEY3Z3acYeFTLCozM6iERfA7_yWxXaiJn7ULwau5vTIpt0Zv04OziMit-/exec"; const IKA_SCRIPT_URL = "https://script.google.com/macros/s/AKfycbwrXG3YFu4PEY3Z3acYeFTLCozM6iERfA7_yWxXaiJn7ULwau5vTIpt0Zv04OziMit-/exec";
// Checkbox visual toggle // Efek visual checkbox kustom pada Form Persetujuan
const consentCheck = document.getElementById('ika-consent'); const consentCheckbox = document.getElementById('ika-consent');
if (consentCheck) { if (consentCheckbox) {
consentCheck.addEventListener('change', function() { consentCheckbox.addEventListener('change', (event) => {
const box = document.getElementById('ika-consent-box'); const consentBoxElement = document.getElementById('ika-consent-box');
if (box) { if (!consentBoxElement) return;
if (this.checked) {
box.innerHTML = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" const isChecked = event.target.checked;
stroke="#2352A0" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"> if (isChecked) {
<polyline points="20 6 9 17 4 12"/></svg>`; consentBoxElement.innerHTML = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none"
} else { stroke="#2352A0" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round">
box.innerHTML = ''; <polyline points="20 6 9 17 4 12"/></svg>`;
} } else {
consentBoxElement.innerHTML = '';
} }
}); });
} }
async function submitIkaForm() { // Mengumpulkan dan merapikan data masukan dari form
const nama = document.getElementById('ika-nama').value.trim(); function getIkaFormData() {
const nim = document.getElementById('ika-nim').value.trim(); return {
const angkatan = document.getElementById('ika-angkatan').value.trim(); nama: document.getElementById('ika-nama').value.trim(),
const hp = document.getElementById('ika-hp').value.trim(); nim: document.getElementById('ika-nim').value.trim(),
const email = document.getElementById('ika-email').value.trim(); angkatan: document.getElementById('ika-angkatan').value.trim(),
const status = document.getElementById('ika-status').value; hp: document.getElementById('ika-hp').value.trim(),
const instansi = document.getElementById('ika-instansi').value.trim(); email: document.getElementById('ika-email').value.trim(),
const skill1 = document.getElementById('ika-skill1').value; status: document.getElementById('ika-status').value,
const skill2 = document.getElementById('ika-skill2').value; instansi: document.getElementById('ika-instansi').value.trim(),
const skill3 = document.getElementById('ika-skill3').value; skill1: document.getElementById('ika-skill1').value,
const consent = document.getElementById('ika-consent').checked; skill2: document.getElementById('ika-skill2').value,
skill3: document.getElementById('ika-skill3').value,
hasConsent: document.getElementById('ika-consent').checked
};
}
const msg = document.getElementById('ika-status-msg'); // Melakukan validasi pengisian field wajib dan persetujuan checkbox
function validateIkaFormData(formData) {
const hasRequiredFields = formData.nama &&
formData.angkatan &&
formData.hp &&
formData.email &&
formData.status &&
formData.skill1 &&
formData.hasConsent;
return !!hasRequiredFields;
}
// Validasi // Memperbarui pesan status pengiriman pada UI
if (!nama || !angkatan || !hp || !email || !status || !skill1 || !consent) { function updateIkaFormStatusMessage(message, color) {
msg.textContent = '⚠️ Lengkapi semua field wajib (*) dan centang persetujuan.'; const statusMessageElement = document.getElementById('ika-status-msg');
msg.style.color = '#ef4444'; if (statusMessageElement) {
return; statusMessageElement.textContent = message;
} statusMessageElement.style.color = color;
const btn = document.getElementById('ika-submit-btn');
btn.disabled = true;
btn.textContent = 'Mengirim...';
msg.textContent = 'Sedang mengirim data...';
msg.style.color = '#9ca3af';
try {
const params = new URLSearchParams({
nama, nim, angkatan, noHp: hp,
email, status, instansi, skill1, skill2, skill3
});
await fetch(IKA_SCRIPT_URL + "?" + params.toString(), {
method: 'GET',
mode: 'no-cors',
});
msg.textContent = '✅ Data berhasil dikirim! Admin akan menghubungimu dalam 12 hari kerja.';
msg.style.color = '#16a34a';
btn.textContent = 'Terkirim ✓';
// Reset form
['ika-nama','ika-nim','ika-angkatan','ika-hp','ika-email','ika-instansi']
.forEach(id => document.getElementById(id).value = '');
['ika-status','ika-skill1','ika-skill2','ika-skill3']
.forEach(id => document.getElementById(id).selectedIndex = 0);
document.getElementById('ika-consent').checked = false;
const consentBox = document.getElementById('ika-consent-box');
if (consentBox) consentBox.innerHTML = '';
} catch (err) {
msg.textContent = '❌ Gagal mengirim. Coba lagi atau hubungi admin.';
msg.style.color = '#ef4444';
btn.disabled = false;
btn.textContent = 'Daftar Sekarang';
} }
} }
/* ── SKILL DISTRIBUTION — Live fetch from Google Sheets (approved rows only) ── */ // Mengubah kondisi tombol kirim dan pesan selama proses mengunggah data
function setIkaFormLoadingState(isLoading) {
const submitButtonElement = document.getElementById('ika-submit-btn');
if (!submitButtonElement) return;
if (isLoading) {
submitButtonElement.disabled = true;
submitButtonElement.textContent = 'Mengirim...';
updateIkaFormStatusMessage('Sedang mengirim data...', '#9ca3af');
} else {
submitButtonElement.disabled = false;
submitButtonElement.textContent = 'Daftar Sekarang';
}
}
// Mengirimkan data form ke Google Apps Script via GET fetch (mode no-cors)
async function sendIkaFormData(formData) {
const params = new URLSearchParams({
nama: formData.nama,
nim: formData.nim,
angkatan: formData.angkatan,
noHp: formData.hp,
email: formData.email,
status: formData.status,
instansi: formData.instansi,
skill1: formData.skill1,
skill2: formData.skill2,
skill3: formData.skill3
});
const targetUrl = `${IKA_SCRIPT_URL}?${params.toString()}`;
await fetch(targetUrl, {
method: 'GET',
mode: 'no-cors'
});
}
// Mereset isian seluruh kolom formulir kembali ke kosong/awal
function resetIkaForm() {
const inputIds = ['ika-nama', 'ika-nim', 'ika-angkatan', 'ika-hp', 'ika-email', 'ika-instansi'];
inputIds.forEach(id => {
const inputElement = document.getElementById(id);
if (inputElement) inputElement.value = '';
});
const selectIds = ['ika-status', 'ika-skill1', 'ika-skill2', 'ika-skill3'];
selectIds.forEach(id => {
const selectElement = document.getElementById(id);
if (selectElement) selectElement.selectedIndex = 0;
});
const consentCheckbox = document.getElementById('ika-consent');
if (consentCheckbox) consentCheckbox.checked = false;
const consentBoxElement = document.getElementById('ika-consent-box');
if (consentBoxElement) consentBoxElement.innerHTML = '';
}
// Alur utama penanganan tombol submit pendaftaran IKA
async function submitIkaForm() {
const formData = getIkaFormData();
const isValid = validateIkaFormData(formData);
if (!isValid) {
updateIkaFormStatusMessage('⚠️ Lengkapi semua field wajib (*) dan centang persetujuan.', '#ef4444');
return;
}
setIkaFormLoadingState(true);
try {
await sendIkaFormData(formData);
updateIkaFormStatusMessage('✅ Data berhasil dikirim! Admin akan menghubungimu dalam 12 hari kerja.', '#16a34a');
const submitButtonElement = document.getElementById('ika-submit-btn');
if (submitButtonElement) {
submitButtonElement.textContent = 'Terkirim ✓';
}
resetIkaForm();
} catch (error) {
updateIkaFormStatusMessage('❌ Gagal mengirim. Coba lagi atau hubungi admin.', '#ef4444');
setIkaFormLoadingState(false);
}
}
// Manajemen Penarikan Data Keahlian Alumni dari Google Sheets secara Live
(function fetchSkillData() { (function fetchSkillData() {
const SHEET_ID = '1mtVB1vLev-OWz6aTuuddGKPS8tCv8MJcGpzws8irpxk'; 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 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}`; 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) // Pemetaan kategori keahlian ke ID element grafik di HTML
const SKILL_MAP = { const SKILL_MAP = {
'Web Development' : 'web-development', 'Web Development': 'web-development',
'Mobile Development' : 'mobile-development', 'Mobile Development': 'mobile-development',
'Python / Data Science': 'python-data-science', 'Python / Data Science': 'python-data-science',
'UI/UX Design' : 'uiux-design', 'UI/UX Design': 'uiux-design',
'Networking / DevOps' : 'networking-devops', 'Networking / DevOps': 'networking-devops',
'Cybersecurity' : 'cybersecurity', 'Cybersecurity': 'cybersecurity',
'Machine Learning / AI': 'machine-learning-ai', 'Machine Learning / AI': 'machine-learning-ai',
'Database' : 'database', 'Database': 'database',
}; };
const spinner = document.getElementById('skill-note-spinner'); const spinnerElement = document.getElementById('skill-note-spinner');
const noteText = document.getElementById('skill-note-text'); const noteTextElement = document.getElementById('skill-note-text');
function setNote(text, spin, isCta) { // Menampilkan catatan status di bawah grafik keahlian
if (spinner) spinner.style.animation = spin ? 'spin 1s linear infinite' : 'none'; function setSkillNote(text, shouldSpin, shouldShowCta) {
if (!noteText) return; if (spinnerElement) {
if (isCta) { spinnerElement.style.animation = shouldSpin ? 'spin 1s linear infinite' : 'none';
noteText.innerHTML = text + ' <a href="#kontribusi" style="color:#2352a0;font-weight:700;text-decoration:underline;text-underline-offset:2px;">Isi form kontribusi</a>'; }
if (!noteTextElement) return;
if (shouldShowCta) {
noteTextElement.innerHTML = `${text} <a href="#kontribusi" style="color:#2352a0;font-weight:700;text-decoration:underline;text-underline-offset:2px;">Isi form kontribusi</a>`;
} else { } else {
noteText.textContent = text; noteTextElement.textContent = text;
} }
} }
function normalizeSkill(val) { // Menyaring dan mengelompokkan teks bebas ke kategori keahlian terstandar
if (!val) return null; function normalizeSkill(rawSkillText) {
const s = val.toLowerCase().trim(); if (!rawSkillText) return null;
if (s.includes('web')) return 'Web Development'; const lowercasedSkill = rawSkillText.toLowerCase().trim();
if (s.includes('mobile')) return 'Mobile Development'; if (lowercasedSkill.includes('web')) return 'Web Development';
if (s.includes('python') || s.includes('data science') || s.includes('data science')) return 'Python / Data Science'; if (lowercasedSkill.includes('mobile')) return 'Mobile Development';
if (s.includes('ui') || s.includes('ux')) return 'UI/UX Design'; if (lowercasedSkill.includes('python') || lowercasedSkill.includes('data science')) return 'Python / Data Science';
if (s.includes('network') || s.includes('devops')) return 'Networking / DevOps'; if (lowercasedSkill.includes('ui') || lowercasedSkill.includes('ux')) return 'UI/UX Design';
if (s.includes('cyber')) return 'Cybersecurity'; if (lowercasedSkill.includes('network') || lowercasedSkill.includes('devops')) return 'Networking / DevOps';
if (s.includes('machine') || s.includes('ai') || s.includes('artificial')) return 'Machine Learning / AI'; if (lowercasedSkill.includes('cyber')) return 'Cybersecurity';
if (s.includes('database') || s.includes('db')) return 'Database'; if (lowercasedSkill.includes('machine') || lowercasedSkill.includes('ai') || lowercasedSkill.includes('artificial')) return 'Machine Learning / AI';
if (lowercasedSkill.includes('database') || lowercasedSkill.includes('db')) return 'Database';
return null; return null;
} }
// Memotong pembungkus JSONP Google Sheets agar diperoleh array baris murni
function parseGoogleSheetsJson(responseText) {
const sanitizedText = responseText.replace(/^[^{]+/, '').replace(/[);]+$/, '');
const parsedData = JSON.parse(sanitizedText);
return (parsedData.table && parsedData.table.rows) ? parsedData.table.rows : [];
}
// Menghitung kemunculan setiap kategori keahlian dari data Sheets
function calculateSkillCounts(rows) {
const counts = {};
rows.forEach(row => {
[0, 1, 2].forEach(colIndex => {
const cell = row.c && row.c[colIndex];
const rawValue = cell && cell.v ? String(cell.v).trim() : '';
const normalized = normalizeSkill(rawValue);
if (normalized) {
counts[normalized] = (counts[normalized] || 0) + 1;
}
});
});
return counts;
}
// Memperbarui lebar progress bar keahlian dan persentase teks di UI
function updateSkillBarsUI(counts) {
const totalMentions = Object.values(counts).reduce((sum, count) => sum + count, 0);
const maxCount = Math.max(...Object.values(counts), 0);
Object.entries(SKILL_MAP).forEach(([label, idKey]) => {
const count = counts[label] || 0;
const percentage = totalMentions > 0 ? Math.round((count / totalMentions) * 100) : 0;
const barPercentage = maxCount > 0 ? Math.round((count / maxCount) * 100) : 0;
const barElement = document.getElementById(`bar-${idKey}`);
const percentElement = document.getElementById(`pct-${idKey}`);
if (barElement) barElement.style.width = `${barPercentage}%`;
if (percentElement) percentElement.textContent = count > 0 ? `${percentage}%` : '—';
});
}
// Melakukan fetch data live dari spreadsheet
fetch(url) fetch(url)
.then(res => res.text()) .then(response => response.text())
.then(raw => { .then(responseText => {
const json = JSON.parse(raw.replace(/^[^{]+/, '').replace(/[);]+$/, '')); const rows = parseGoogleSheetsJson(responseText);
const rows = (json.table && json.table.rows) ? json.table.rows : [];
if (rows.length === 0) { if (rows.length === 0) {
setNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false); setSkillNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false);
return; return;
} }
// Count normalized skill mentions const counts = calculateSkillCounts(rows);
const counts = {}; updateSkillBarsUI(counts);
rows.forEach(row => { setSkillNote(`Data dari ${rows.length} alumni yang diverifikasi.`, false, false);
[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(() => { .catch(() => {
setNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false); setSkillNote('Data skill masih kosong, bantu kami lengkapi data ini!', false, false);
}); });
})(); })();
/* ── REVEAL ON SCROLL ── */ // Observer Efek Muncul Perlahan saat Pengguliran Halaman (Scroll Reveal)
const ro = new IntersectionObserver( const revealObserver = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((e) => { entries.forEach((entry) => {
if (e.isIntersecting) e.target.classList.add("in"); if (entry.isIntersecting) {
entry.target.classList.add("in");
}
}); });
}, },
{ threshold: 0.08 }, { threshold: 0.08 }
); );
document.querySelectorAll(".rv").forEach((el) => ro.observe(el)); document.querySelectorAll(".rv").forEach((el) => revealObserver.observe(el));
/* ── FAQ TOGGLE ── */ // Menampilkan atau menyembunyikan panel jawaban FAQ
function toggleFaq(item) { function toggleFaq(item) {
item.classList.toggle("open"); item.classList.toggle("open");
} }
/* ── CHART.JS WITH BILINGUAL SUPPORT ── */ // Inisialisasi Grafik Jumlah Kelulusan (Chart.js) dengan Dukungan Bilingual
window.addEventListener("load", function () { window.addEventListener("load", () => {
if (typeof Chart === "undefined") return; if (typeof Chart === "undefined") return;
const ctx = document.getElementById("kelulusanChart"); const chartCanvasElement = document.getElementById("kelulusanChart");
if (!ctx) return; if (!chartCanvasElement) return;
const getChartLabels = (lang) => { // Mengembalikan nama label kuartal berdasarkan pilihan bahasa aktif
if (lang === 'en') { const getChartLabels = (languageCode) => {
return ['2022 Even', '2023 Odd', '2023 Even', '2024 Odd', '2024 Even']; return languageCode === 'en'
} ? ['2022 Even', '2023 Odd', '2023 Even', '2024 Odd', '2024 Even']
return ['2022 Genap', '2023 Ganjil', '2023 Genap', '2024 Ganjil', '2024 Genap']; : ['2022 Genap', '2023 Ganjil', '2023 Genap', '2024 Ganjil', '2024 Genap'];
}; };
const initialLang = typeof getLang === 'function' ? getLang() : 'id'; const initialLanguageCode = typeof getLang === 'function' ? getLang() : 'id';
window.kelulusanChartInstance = new Chart(ctx.getContext("2d"), { window.kelulusanChartInstance = new Chart(chartCanvasElement.getContext("2d"), {
type: "bar", type: "bar",
data: { data: {
labels: getChartLabels(initialLang), labels: getChartLabels(initialLanguageCode),
datasets: [ datasets: [
{ {
label: initialLang === 'en' ? 'Number of Graduates' : 'Jumlah Lulusan', label: initialLanguageCode === 'en' ? 'Number of Graduates' : 'Jumlah Lulusan',
data: [45, 27, 63, 27, 50], data: [45, 27, 63, 27, 50],
backgroundColor: "#299bc1", // sesuai dengan warna biru pada gambar backgroundColor: "#299bc1",
borderRadius: 0, borderRadius: 0,
borderSkipped: false, borderSkipped: false,
} }
@@ -2933,9 +2998,9 @@
cornerRadius: 8, cornerRadius: 8,
callbacks: { callbacks: {
label: (context) => { label: (context) => {
const lang = typeof getLang === 'function' ? getLang() : 'id'; const languageCode = typeof getLang === 'function' ? getLang() : 'id';
const text = lang === 'en' ? 'Graduates' : 'Jumlah'; const text = languageCode === 'en' ? 'Graduates' : 'Jumlah';
const unit = lang === 'en' ? 'People' : 'Orang'; const unit = languageCode === 'en' ? 'People' : 'Orang';
return ` ${text}: ${context.raw} ${unit}`; return ` ${text}: ${context.raw} ${unit}`;
}, },
}, },
@@ -2966,12 +3031,12 @@
}, },
}); });
// Listen for languageChanged event to dynamically update the chart // Memperbarui grafik secara dinamis saat terjadi event pergantian bahasa
document.addEventListener('languageChanged', (e) => { document.addEventListener('languageChanged', (event) => {
const lang = e.detail.lang; const languageCode = event.detail.lang;
if (window.kelulusanChartInstance) { if (window.kelulusanChartInstance) {
window.kelulusanChartInstance.data.labels = getChartLabels(lang); window.kelulusanChartInstance.data.labels = getChartLabels(languageCode);
window.kelulusanChartInstance.data.datasets[0].label = lang === 'en' ? 'Number of Graduates' : 'Jumlah Lulusan'; window.kelulusanChartInstance.data.datasets[0].label = languageCode === 'en' ? 'Number of Graduates' : 'Jumlah Lulusan';
window.kelulusanChartInstance.update(); window.kelulusanChartInstance.update();
} }
}); });
+31 -1
View File
@@ -593,6 +593,12 @@ a {
justify-content: center; justify-content: center;
border: 3px solid var(--navy-ghost); border: 3px solid var(--navy-ghost);
margin-bottom: 12px; margin-bottom: 12px;
overflow: hidden;
}
.achiever-avatar-circle img {
width: 100%;
height: 100%;
object-fit: cover;
} }
.achiever-card:hover .achiever-avatar-circle { .achiever-card:hover .achiever-avatar-circle {
border-color: var(--gold-lt); border-color: var(--gold-lt);
@@ -660,11 +666,15 @@ ul.achiever-achievements li::before {
gap: 52px; gap: 52px;
align-items: start; align-items: start;
} }
.stat-cols {
grid-template-columns: 1.3fr 0.7fr;
}
.two-col > * { .two-col > * {
min-width: 0; min-width: 0;
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.two-col { .two-col,
.stat-cols {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 32px; gap: 32px;
} }
@@ -801,6 +811,19 @@ ul.achiever-achievements li::before {
min-width: 34px; min-width: 34px;
text-align: right; text-align: right;
} }
@media (max-width: 500px) {
.skill-label {
min-width: 120px;
font-size: 0.78rem;
}
.skill-row {
gap: 8px;
}
.skill-pct {
font-size: 0.78rem;
min-width: 30px;
}
}
/* skill stat box */ /* skill stat box */
.skill-stat-box { .skill-stat-box {
@@ -1837,6 +1860,13 @@ ul.achiever-achievements li::before {
#assignment-body { #assignment-body {
min-width: 0; min-width: 0;
} }
@media (max-width: 768px) {
#assignment-body {
padding-left: 16px;
padding-right: 16px;
box-sizing: border-box;
}
}
/* ===================================================== /* =====================================================
CUSTOM HERO FOR M4N1FEST CUSTOM HERO FOR M4N1FEST