forked from izu/student-web-if-development-kit
Compare commits
32 Commits
rehan
..
bd75009c8b
| Author | SHA1 | Date | |
|---|---|---|---|
| bd75009c8b | |||
| f45f14b60b | |||
| d34cc1c6e5 | |||
| 68cd20864e | |||
| b3a34dd608 | |||
| 99de2a3167 | |||
| b65a581ed4 | |||
| 986477ab58 | |||
| 4375c846fa | |||
| 30091e68cc | |||
| 91136a58a1 | |||
| 478d48c237 | |||
| 56c34eddc3 | |||
| 3b749e4c99 | |||
| 1d5b807fc7 | |||
| 97a7cd1369 | |||
| 793a9e1dbc | |||
| 11b8e518ac | |||
| 6f1e7d744b | |||
| 48ff6250ab | |||
| d1a80838a5 | |||
| c64abd8083 | |||
| 6cb0ffc571 | |||
| 46a7921bf0 | |||
| fbdcbe6472 | |||
| 263ffd1924 | |||
| 123ce1be04 | |||
| 3dd522de3d | |||
| f27b2a901a | |||
| 3c54047474 | |||
| c9ae01f9ae | |||
| 0173fbb679 |
@@ -20,6 +20,16 @@ Buka URL dari Vite (default: `http://localhost:5173`) lalu akses launcher di `/`
|
|||||||
| `npm run build` | Build static output ke folder `dist` |
|
| `npm run build` | Build static output ke folder `dist` |
|
||||||
| `npm run preview` | Preview hasil build |
|
| `npm run preview` | Preview hasil build |
|
||||||
|
|
||||||
|
## Deploy Netlify
|
||||||
|
|
||||||
|
Project ini sudah punya konfigurasi Netlify di `netlify.toml`.
|
||||||
|
|
||||||
|
Saat membuat site di Netlify:
|
||||||
|
- Repository: `GuavaPopper/PPL_Staging`
|
||||||
|
- Branch deploy: `Staging`
|
||||||
|
- Build command: `npm run build`
|
||||||
|
- Publish directory: `dist`
|
||||||
|
|
||||||
## Struktur Folder
|
## Struktur Folder
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+1173
-494
File diff suppressed because it is too large
Load Diff
+320
-105
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="id">
|
<html lang="id">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@@ -74,6 +74,8 @@
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
window.APP_CONFIG = window.APP_CONFIG || {
|
window.APP_CONFIG = window.APP_CONFIG || {
|
||||||
|
debugLogs: false,
|
||||||
|
dashboardRuntimeFetch: 'local',
|
||||||
dashboardTargetUrl: 'https://dashboard.informatika.untan.ac.id/index.php?penelitian',
|
dashboardTargetUrl: 'https://dashboard.informatika.untan.ac.id/index.php?penelitian',
|
||||||
dashboardProxyTemplates: [
|
dashboardProxyTemplates: [
|
||||||
'https://api.allorigins.win/get?url={{url}}&ts={{ts}}',
|
'https://api.allorigins.win/get?url={{url}}&ts={{ts}}',
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<!-- RODA_DASHBOARD_DATA -->
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -130,9 +133,8 @@
|
|||||||
class="hover:text-untan-yellow transition-colors font-medium">SATU UNTAN</a>
|
class="hover:text-untan-yellow transition-colors font-medium">SATU UNTAN</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pl-5 border-l border-white/20 flex items-center gap-3">
|
<div class="pl-5 border-l border-white/20 flex items-center gap-3">
|
||||||
<button id="lang-toggle-btn"
|
<button
|
||||||
class="language-toggle-btn text-xs hover:text-untan-yellow transition-colors flex items-center gap-1"
|
class="language-toggle-btn text-xs hover:text-untan-yellow transition-colors flex items-center gap-1">
|
||||||
onclick="setLanguage(currentLang === 'id' ? 'en' : 'id')">
|
|
||||||
<span class="font-bold">ID</span> <span class="text-white/40">|</span> EN
|
<span class="font-bold">ID</span> <span class="text-white/40">|</span> EN
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -430,11 +432,138 @@
|
|||||||
<div id="mega-overlay" class="fixed inset-0 bg-black/30 backdrop-blur-sm z-30"></div>
|
<div id="mega-overlay" class="fixed inset-0 bg-black/30 backdrop-blur-sm z-30"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumb (Diubah menyesuaikan pepet kiri navbar) -->
|
<!-- Mobile Menu -->
|
||||||
<div class="bg-white">
|
<div id="mobile-menu"
|
||||||
<div class="w-full px-3 sm:px-4 md:px-6 lg:px-8 xl:px-10 py-3 sm:py-3 md:py-3.5 lg:py-3.5 xl:py-3.5 text-[10px] sm:text-[11px] md:text-xs lg:text-xs xl:text-xs font-medium text-gray-500 uppercase">
|
class="fixed inset-0 bg-slate-900 z-[60] transform -translate-x-full lg:hidden flex flex-col transition-transform duration-300"
|
||||||
<span data-i18n="home">BERANDA</span> <span class="mx-2">/</span> <span class="text-untan-blue font-bold"
|
role="dialog" aria-modal="true" aria-label="Menu navigasi mobile">
|
||||||
data-i18n="pageTitle">RISET & KBK</span>
|
<div class="p-4 flex justify-between items-center border-b border-white/10 bg-untan-navy">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span class="font-bold text-lg text-white font-sans">Menu Utama</span>
|
||||||
|
<button
|
||||||
|
class="language-toggle-btn px-2 py-1 rounded-sm border border-white/20 text-xs text-white hover:bg-white/10 transition-colors flex items-center gap-2"
|
||||||
|
aria-label="Ganti bahasa">
|
||||||
|
<span class="font-bold">ID</span> <span class="text-white/40" aria-hidden="true">|</span> EN
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-8 h-8 rounded-sm bg-white/10 hover:bg-white/20 flex items-center justify-center text-white transition-colors"
|
||||||
|
aria-label="Pengaturan aksesibilitas"
|
||||||
|
onclick="const p=document.getElementById('access-panel'); if (p) { p.classList.toggle('hidden'); p.classList.toggle('flex'); }">
|
||||||
|
<i class="fas fa-universal-access" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button id="close-mobile-menu"
|
||||||
|
class="w-8 h-8 rounded-sm bg-white/10 flex items-center justify-center text-white"
|
||||||
|
aria-label="Tutup menu navigasi">
|
||||||
|
<i class="fas fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto p-5 space-y-4 bg-slate-50">
|
||||||
|
<a href="../../index.html" class="block text-base font-bold text-slate-800 py-1 border-b border-slate-200"
|
||||||
|
data-i18n="nav_home">Beranda</a>
|
||||||
|
|
||||||
|
<!-- Tentang Kami -->
|
||||||
|
<div class="pt-2">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans" data-i18n="nav_about">Tentang
|
||||||
|
Kami</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_history">Sejarah & Tonggak</a>
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_org">Struktur Organisasi</a>
|
||||||
|
<a href="profil.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_vision">Visi & Misi</a>
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_partnership">Kerjasama & MoU</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Program -->
|
||||||
|
<div class="pt-2 border-t border-slate-200">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans"
|
||||||
|
data-i18n="nav_program">Program</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="subject-and-courses.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_academic">Akademik</a>
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_accreditation">Akreditasi</a>
|
||||||
|
<a href="graduate-profile.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_graduate">Profil Lulusan</a>
|
||||||
|
<a href="subject-and-courses.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_courses">Mata Kuliah</a>
|
||||||
|
<a href="academic-guidelines.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_guideline">Panduan Akademik</a>
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_statistics">Pengumuman</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SDM -->
|
||||||
|
<div class="pt-2 border-t border-slate-200">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans" data-i18n="nav_sdm">SDM</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="people-based-on-kk.html"
|
||||||
|
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue font-medium"
|
||||||
|
data-i18n="btn_all_lecturers">Semua Dosen</a>
|
||||||
|
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="kk_ai">Kecerdasan Buatan</a>
|
||||||
|
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="kk_se">Rekayasa Perangkat Lunak</a>
|
||||||
|
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="kk_network">Jaringan & Keamanan</a>
|
||||||
|
<a href="people-based-on-kk.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="kk_is">Sistem Informasi</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fasilitas -->
|
||||||
|
<div class="pt-2 border-t border-slate-200">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans"
|
||||||
|
data-i18n="nav_facilities">Fasilitas</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="mega_lab">Laboratorium</a>
|
||||||
|
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="mega_dc">Data Center</a>
|
||||||
|
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="mega_eresources">E-Resources</a>
|
||||||
|
<a href="ifuntanhub.html"
|
||||||
|
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue">IFUNTANHUB</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Karya & Agenda -->
|
||||||
|
<div class="pt-2 border-t border-slate-200">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans" data-i18n="nav_works">Karya &
|
||||||
|
Agenda</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="karya.html" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_all_karya">Semua Karya</a>
|
||||||
|
<a href="karya.html?filter=penelitian"
|
||||||
|
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_research">Riset</a>
|
||||||
|
<a href="karya.html?filter=pkm" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_community">Pengabdian Masyarakat</a>
|
||||||
|
<a href="content.html?type=rubrik" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_achievements">Prestasi</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kontak -->
|
||||||
|
<div class="pt-2 border-t border-slate-200 pb-8">
|
||||||
|
<span class="text-xs font-bold text-slate-400 uppercase font-sans"
|
||||||
|
data-i18n="nav_contact">Kontak</span>
|
||||||
|
<div class="pl-2 border-l-2 border-slate-100 mt-1 space-y-1">
|
||||||
|
<a href="content.html?type=berita" class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_official_contact">Kontak Resmi</a>
|
||||||
|
<a href="mailto:info@informatika.untan.ac.id"
|
||||||
|
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue"
|
||||||
|
data-i18n="link_feedback">Saran & Masukan</a>
|
||||||
|
<a href="https://instagram.com/informatika.untan" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="block py-1.5 text-sm text-slate-700 hover:text-untan-blue flex items-center gap-2">
|
||||||
|
<i class="fab fa-instagram text-pink-500"></i> Instagram
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -483,7 +612,7 @@
|
|||||||
topik riset spesifik, dipimpin dosen berpengalaman, dan terbuka untuk kolaborasi.</p>
|
topik riset spesifik, dipimpin dosen berpengalaman, dan terbuka untuk kolaborasi.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="kbk-card-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8">
|
<div id="kbk-card-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-5 md:gap-6 lg:gap-7 xl:gap-8">
|
||||||
<!-- AI Card -->
|
<!-- AI Card -->
|
||||||
<article data-kbk-card="ai"
|
<article data-kbk-card="ai"
|
||||||
class="bg-white p-5 sm:p-6 md:p-7 lg:p-8 xl:p-8 rounded-2xl sm:rounded-2.5xl md:rounded-3xl lg:rounded-3xl xl:rounded-3xl shadow-soft border border-gray-100 hover:shadow-soft-lg hover:border-untan-yellow transition-all duration-500 group flex flex-col h-full border-t-4 border-t-untan-navy">
|
class="bg-white p-5 sm:p-6 md:p-7 lg:p-8 xl:p-8 rounded-2xl sm:rounded-2.5xl md:rounded-3xl lg:rounded-3xl xl:rounded-3xl shadow-soft border border-gray-100 hover:shadow-soft-lg hover:border-untan-yellow transition-all duration-500 group flex flex-col h-full border-t-4 border-t-untan-navy">
|
||||||
@@ -791,6 +920,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p><span data-i18n="lastUpdatedText">This page was last updated on: </span><span id="last-updated"></span></p>
|
||||||
|
|
||||||
<div id="riset-count-info"
|
<div id="riset-count-info"
|
||||||
class="mt-3 sm:mt-3.5 md:mt-4 lg:mt-3 xl:mt-4 text-[10px] sm:text-[10px] md:text-[11px] lg:text-[11px] xl:text-[11px] font-medium text-slate-400 uppercase tracking-widest">
|
class="mt-3 sm:mt-3.5 md:mt-4 lg:mt-3 xl:mt-4 text-[10px] sm:text-[10px] md:text-[11px] lg:text-[11px] xl:text-[11px] font-medium text-slate-400 uppercase tracking-widest">
|
||||||
Menghitung data...
|
Menghitung data...
|
||||||
@@ -798,7 +929,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="riset-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5 md:gap-6 lg:gap-8 xl:gap-8 mb-10 sm:mb-12 md:mb-14 lg:mb-16 xl:mb-20">
|
<div id="riset-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-5 md:gap-6 lg:gap-8 xl:gap-8 mb-10 sm:mb-12 md:mb-14 lg:mb-16 xl:mb-20">
|
||||||
<!-- Cards generated by JS -->
|
<!-- Cards generated by JS -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -825,7 +956,7 @@
|
|||||||
ketua setiap KBK. Pilih kartu untuk melihat dosen lain di kelompok keahlian tersebut.</p>
|
ketua setiap KBK. Pilih kartu untuk melihat dosen lain di kelompok keahlian tersebut.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dosen-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6 md:gap-7 lg:gap-8 xl:gap-8">
|
<div id="dosen-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5 sm:gap-6 md:gap-7 lg:gap-8 xl:gap-8">
|
||||||
<!-- Cards generated by JS -->
|
<!-- Cards generated by JS -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -838,7 +969,7 @@
|
|||||||
<div class="max-w-2xl">
|
<div class="max-w-2xl">
|
||||||
<h2 class="text-2xl sm:text-2.5xl md:text-3xl lg:text-4xl xl:text-5xl font-bold font-condensed text-untan-navy mb-5 sm:mb-6 md:mb-7 lg:mb-8 xl:mb-8 leading-tight"
|
<h2 class="text-2xl sm:text-2.5xl md:text-3xl lg:text-4xl xl:text-5xl font-bold font-condensed text-untan-navy mb-5 sm:mb-6 md:mb-7 lg:mb-8 xl:mb-8 leading-tight"
|
||||||
data-i18n="labTitle">Laboratorium Pendukung Riset</h2>
|
data-i18n="labTitle">Laboratorium Pendukung Riset</h2>
|
||||||
<p class="text-slate-500 text-xs sm:text-sm md:text-base lg:text-base xl:text-lg leading-relaxed">5 laboratorium modern dengan 93 unit PC All-in-One
|
<p class="text-slate-500 text-xs sm:text-sm md:text-base lg:text-base xl:text-lg leading-relaxed" data-i18n="labDesc">5 laboratorium modern dengan 93 unit PC All-in-One
|
||||||
yang mendukung kegiatan riset strategis dan pengembangan teknologi dosen serta mahasiswa.
|
yang mendukung kegiatan riset strategis dan pengembangan teknologi dosen serta mahasiswa.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1195,7 +1326,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const translations = {
|
const rodaTranslations = {
|
||||||
id: {
|
id: {
|
||||||
info: 'INFO TERBARU',
|
info: 'INFO TERBARU',
|
||||||
tickerItem1: 'Selamat datang di antarmuka situs web Informatika terbaru!',
|
tickerItem1: 'Selamat datang di antarmuka situs web Informatika terbaru!',
|
||||||
@@ -1250,7 +1381,10 @@
|
|||||||
ctaBtn: 'Hubungi Kami',
|
ctaBtn: 'Hubungi Kami',
|
||||||
viewAll: 'Lihat Semua',
|
viewAll: 'Lihat Semua',
|
||||||
facultyDirectory: 'Direktori Dosen',
|
facultyDirectory: 'Direktori Dosen',
|
||||||
expertise: 'Keahlian'
|
expertise: 'Keahlian',
|
||||||
|
lastUpdatedText: 'Halaman ini terakhir diperbarui pada: ',
|
||||||
|
showingPubs: 'Menampilkan {0} dari {1} publikasi',
|
||||||
|
labDesc: '5 laboratorium modern dengan 93 unit PC All-in-One yang mendukung kegiatan riset strategis dan pengembangan teknologi dosen serta mahasiswa.'
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
info: 'LATEST INFO',
|
info: 'LATEST INFO',
|
||||||
@@ -1306,11 +1440,14 @@
|
|||||||
ctaBtn: 'Contact Us',
|
ctaBtn: 'Contact Us',
|
||||||
viewAll: 'View All',
|
viewAll: 'View All',
|
||||||
facultyDirectory: 'Faculty Directory',
|
facultyDirectory: 'Faculty Directory',
|
||||||
expertise: 'Expertise'
|
expertise: 'Expertise',
|
||||||
|
lastUpdatedText: 'This page was last updated on: ',
|
||||||
|
showingPubs: 'Showing {0} of {1} publications',
|
||||||
|
labDesc: '5 modern laboratories with 93 All-in-One PCs supporting strategic research and technological development for faculty and students.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentLang = 'id';
|
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let currentQuery = '';
|
let currentQuery = '';
|
||||||
let currentKbkInsight = 'ai';
|
let currentKbkInsight = 'ai';
|
||||||
@@ -1335,6 +1472,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logAppError(scope, error, extra = {}) {
|
function logAppError(scope, error, extra = {}) {
|
||||||
|
if (window.APP_CONFIG?.debugLogs !== true) return;
|
||||||
console.error(`[${scope}]`, error, extra);
|
console.error(`[${scope}]`, error, extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1345,6 +1483,8 @@
|
|||||||
if (error.message === 'INVALID_RESPONSE') return 'Respons dari proxy tidak valid dan tidak bisa diproses.';
|
if (error.message === 'INVALID_RESPONSE') return 'Respons dari proxy tidak valid dan tidak bisa diproses.';
|
||||||
if (error.message === 'EMPTY_RESPONSE') return 'Dashboard mengembalikan data kosong atau struktur halaman berubah.';
|
if (error.message === 'EMPTY_RESPONSE') return 'Dashboard mengembalikan data kosong atau struktur halaman berubah.';
|
||||||
if (error.message === 'NO_ROWS_FOUND') return 'Data penelitian tidak ditemukan pada halaman dashboard.';
|
if (error.message === 'NO_ROWS_FOUND') return 'Data penelitian tidak ditemukan pada halaman dashboard.';
|
||||||
|
if (error.message === 'DASHBOARD_PROXY_NOT_CONFIGURED') return 'Proxy dashboard belum dikonfigurasi.';
|
||||||
|
if (error.message === 'DASHBOARD_RUNTIME_FETCH_DISABLED') return 'Data publikasi belum tersedia pada build ini.';
|
||||||
if (error.message?.startsWith('HTTP_ERROR_')) return `Proxy merespons gagal (${error.message.replace('HTTP_ERROR_', 'HTTP ')}).`;
|
if (error.message?.startsWith('HTTP_ERROR_')) return `Proxy merespons gagal (${error.message.replace('HTTP_ERROR_', 'HTTP ')}).`;
|
||||||
return 'Gagal mengambil data. Server dashboard mungkin sedang sibuk atau memblokir akses.';
|
return 'Gagal mengambil data. Server dashboard mungkin sedang sibuk atau memblokir akses.';
|
||||||
}
|
}
|
||||||
@@ -1405,56 +1545,30 @@
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDashboardData() {
|
function shouldUseLocalDashboardProxy() {
|
||||||
const grid = document.getElementById('riset-grid');
|
const hostname = window.location.hostname;
|
||||||
if (grid) {
|
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '';
|
||||||
grid.innerHTML = `
|
|
||||||
<div class="md:col-span-2 lg:col-span-3 py-20 text-center">
|
|
||||||
<div class="inline-block animate-spin w-10 h-10 border-4 border-untan-blue border-t-transparent rounded-full mb-4"></div>
|
|
||||||
<p class="text-slate-500 font-bold">Mengambil data dari Dashboard...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetUrl = window.APP_CONFIG?.dashboardTargetUrl?.trim();
|
function isDashboardRuntimeFetchAllowed() {
|
||||||
const proxyTemplates = Array.isArray(window.APP_CONFIG?.dashboardProxyTemplates)
|
const mode = window.APP_CONFIG?.dashboardRuntimeFetch;
|
||||||
? window.APP_CONFIG.dashboardProxyTemplates.filter(Boolean)
|
return mode === true || mode === 'always' || (mode === 'local' && shouldUseLocalDashboardProxy());
|
||||||
: [];
|
|
||||||
|
|
||||||
// Sumber utama: endpoint proxy lokal (server.js fetch server-side, tanpa CORS).
|
|
||||||
const proxyServers = [() => '/api/dashboard'];
|
|
||||||
|
|
||||||
// Fallback: proxy publik (butuh targetUrl untuk membangun URL).
|
|
||||||
if (targetUrl) {
|
|
||||||
proxyTemplates.forEach((template) => {
|
|
||||||
proxyServers.push((url) => template
|
|
||||||
.replaceAll('{{url}}', encodeURIComponent(url))
|
|
||||||
.replaceAll('{{rawUrl}}', url)
|
|
||||||
.replaceAll('{{ts}}', String(Date.now())));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastError = null;
|
function readEmbeddedDashboardHtml() {
|
||||||
|
const source = document.getElementById('roda-dashboard-html');
|
||||||
for (const getProxyUrl of proxyServers) {
|
if (!source) return '';
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxyUrl = getProxyUrl(targetUrl);
|
return JSON.parse(source.textContent || '""') || '';
|
||||||
console.log(`Trying proxy: ${proxyUrl}`);
|
} catch (error) {
|
||||||
|
return source.textContent || '';
|
||||||
const response = await fetch(proxyUrl, { signal: controller.signal });
|
}
|
||||||
clearTimeout(timeoutId);
|
}
|
||||||
|
|
||||||
if (!response.ok) throw new Error(`HTTP_ERROR_${response.status}`);
|
|
||||||
|
|
||||||
const responseData = await response.text();
|
|
||||||
const html = extractDashboardHtml(responseData);
|
|
||||||
const rows = parseDashboardRows(html);
|
|
||||||
|
|
||||||
|
function applyDashboardRows(rows) {
|
||||||
const newData = [];
|
const newData = [];
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row) => {
|
||||||
const cols = row.querySelectorAll('td');
|
const cols = row.querySelectorAll('td');
|
||||||
if (cols.length >= 3) {
|
if (cols.length >= 3) {
|
||||||
const indexVal = cols[0].innerText.trim();
|
const indexVal = cols[0].innerText.trim();
|
||||||
@@ -1495,58 +1609,96 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newData.length > 0) {
|
if (!newData.length) {
|
||||||
|
throw new Error('NO_ROWS_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
risetData = newData;
|
risetData = newData;
|
||||||
isLoadingData = false;
|
isLoadingData = false;
|
||||||
renderRisetCards();
|
renderRisetCards();
|
||||||
updateStatPub(newData.length);
|
updateStatPub(newData.length);
|
||||||
return; // Berhasil! Keluar dari loop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('NO_ROWS_FOUND');
|
async function fetchDashboardData() {
|
||||||
|
const grid = document.getElementById('riset-grid');
|
||||||
|
if (grid) {
|
||||||
|
grid.innerHTML = `
|
||||||
|
<div class="md:col-span-2 lg:col-span-3 py-20 text-center">
|
||||||
|
<div class="inline-block animate-spin w-10 h-10 border-4 border-untan-blue border-t-transparent rounded-full mb-4"></div>
|
||||||
|
<p class="text-slate-500 font-bold">Mengambil data dari Dashboard...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError = null;
|
||||||
|
const embeddedHtml = readEmbeddedDashboardHtml();
|
||||||
|
if (embeddedHtml) {
|
||||||
|
try {
|
||||||
|
applyDashboardRows(parseDashboardRows(embeddedHtml));
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDashboardRuntimeFetchAllowed()) {
|
||||||
|
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError || new Error('DASHBOARD_RUNTIME_FETCH_DISABLED')), 'Coba Lagi', 'fetchDashboardData()');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUrl = window.APP_CONFIG?.dashboardTargetUrl?.trim();
|
||||||
|
const proxyTemplates = Array.isArray(window.APP_CONFIG?.dashboardProxyTemplates)
|
||||||
|
? window.APP_CONFIG.dashboardProxyTemplates.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Endpoint server.js hanya ada saat development lokal.
|
||||||
|
const proxyServers = shouldUseLocalDashboardProxy() ? [() => '/api/dashboard'] : [];
|
||||||
|
|
||||||
|
// Deployment static memakai proxy publik karena tidak menjalankan server.js.
|
||||||
|
if (targetUrl) {
|
||||||
|
proxyTemplates.forEach((template) => {
|
||||||
|
proxyServers.push((url) => template
|
||||||
|
.replaceAll('{{url}}', encodeURIComponent(url))
|
||||||
|
.replaceAll('{{rawUrl}}', url)
|
||||||
|
.replaceAll('{{ts}}', String(Date.now())));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxyServers.length) {
|
||||||
|
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(new Error('DASHBOARD_PROXY_NOT_CONFIGURED')), 'Coba Lagi', 'fetchDashboardData()');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const getProxyUrl of proxyServers) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 15000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const proxyUrl = getProxyUrl(targetUrl);
|
||||||
|
|
||||||
|
const response = await fetch(proxyUrl, { signal: controller.signal });
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`HTTP_ERROR_${response.status}`);
|
||||||
|
|
||||||
|
const responseData = await response.text();
|
||||||
|
const html = extractDashboardHtml(responseData);
|
||||||
|
const rows = parseDashboardRows(html);
|
||||||
|
applyDashboardRows(rows);
|
||||||
|
return; // Berhasil! Keluar dari loop
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
logAppError('dashboard-proxy', error, { targetUrl });
|
|
||||||
lastError = error;
|
lastError = error;
|
||||||
// Lanjut ke proxy berikutnya...
|
// Lanjut ke proxy berikutnya...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika semua proxy gagal
|
// Jika semua proxy gagal
|
||||||
logAppError('fetchDashboardData', lastError);
|
|
||||||
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError), 'Coba Lagi', 'fetchDashboardData()');
|
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError), 'Coba Lagi', 'fetchDashboardData()');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateLangButton() {
|
// Global localization.js handles translations and language toggle
|
||||||
const desktopButton = document.getElementById('lang-toggle-btn');
|
|
||||||
if (desktopButton) {
|
|
||||||
desktopButton.innerHTML = currentLang === 'id'
|
|
||||||
? '<span class="font-bold text-white">ID</span> <span class="text-white/40 mx-2">|</span> <span class="font-normal text-white">EN</span>'
|
|
||||||
: '<span class="font-normal text-white">ID</span> <span class="text-white/40 mx-2">|</span> <span class="font-bold text-white">EN</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const mobileButton = document.getElementById('mobile-lang-toggle-btn');
|
|
||||||
if (mobileButton) {
|
|
||||||
mobileButton.textContent = currentLang === 'id' ? 'Bahasa: ID' : 'Language: EN';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTranslation(lang) {
|
|
||||||
document.querySelectorAll('[data-i18n]').forEach((element) => {
|
|
||||||
const key = element.getAttribute('data-i18n');
|
|
||||||
if (translations[lang][key]) {
|
|
||||||
element.innerText = translations[lang][key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
|
|
||||||
const key = element.getAttribute('data-i18n-placeholder');
|
|
||||||
if (translations[lang][key]) {
|
|
||||||
element.setAttribute('placeholder', translations[lang][key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesQuery(item, query) {
|
function matchesQuery(item, query) {
|
||||||
const haystack = [item.title_id, item.title_en, item.authors, item.venue, item.year].join(' ').toLowerCase();
|
const haystack = [item.title_id, item.title_en, item.authors, item.venue, item.year].join(' ').toLowerCase();
|
||||||
@@ -1568,7 +1720,8 @@
|
|||||||
// Update info jumlah data yang ditampilkan
|
// Update info jumlah data yang ditampilkan
|
||||||
const countInfo = document.getElementById('riset-count-info');
|
const countInfo = document.getElementById('riset-count-info');
|
||||||
if (countInfo) {
|
if (countInfo) {
|
||||||
countInfo.innerText = `Menampilkan ${itemsToShow.length} dari ${filteredItems.length} publikasi`;
|
const template = translations[currentLang].showingPubs || `Menampilkan {0} dari {1} publikasi`;
|
||||||
|
countInfo.innerText = template.replace('{0}', itemsToShow.length).replace('{1}', filteredItems.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tampilkan/Sembunyikan tombol "Lihat Semua" berdasarkan sisa data
|
// Tampilkan/Sembunyikan tombol "Lihat Semua" berdasarkan sisa data
|
||||||
@@ -1591,9 +1744,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
grid.innerHTML = itemsToShow.map((item) => `
|
grid.innerHTML = itemsToShow.map((item) => `
|
||||||
<a href="${item.url}" target="_blank" class="bg-white rounded-3xl p-8 shadow-soft border border-gray-100 flex flex-col h-full hover:shadow-soft-lg hover:border-untan-yellow transition-all duration-500 group">
|
<a href="${item.url}" target="_blank" class="bg-white rounded-3xl p-6 sm:p-8 shadow-soft border border-gray-100 flex flex-col h-full hover:shadow-soft-lg hover:border-untan-yellow transition-all duration-500 group">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<span class="bg-untan-blue/5 text-untan-blue text-[10px] font-bold px-4 py-1.5 rounded-full uppercase tracking-wider border border-untan-blue/10">${currentLang === 'id' ? item.badge_id : item.badge_en}</span>
|
<span class="inline-block max-w-full bg-untan-blue/5 text-untan-blue text-[10px] font-bold px-4 py-1.5 rounded-full uppercase tracking-wider leading-tight text-center border border-untan-blue/10">${currentLang === 'id' ? item.badge_id : item.badge_en}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-bold text-untan-navy mb-4 leading-snug line-clamp-3 group-hover:text-untan-blue transition-colors">${currentLang === 'id' ? item.title_id : item.title_en}</h3>
|
<h3 class="text-lg font-bold text-untan-navy mb-4 leading-snug line-clamp-3 group-hover:text-untan-blue transition-colors">${currentLang === 'id' ? item.title_id : item.title_en}</h3>
|
||||||
<p class="text-gray-500 text-xs mb-1 italic">${item.authors}</p>
|
<p class="text-gray-500 text-xs mb-1 italic">${item.authors}</p>
|
||||||
@@ -1627,7 +1780,7 @@
|
|||||||
const rank = currentLang === 'id' ? leader.badge_id : leader.badge_en;
|
const rank = currentLang === 'id' ? leader.badge_id : leader.badge_en;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<a href="dosen_kbk.html?kbk=${kbk}&lang=${currentLang}" class="group block rounded-2xl sm:rounded-2.5xl md:rounded-3xl lg:rounded-3xl xl:rounded-3xl overflow-hidden border border-slate-100 bg-white shadow-soft hover:-translate-y-2 hover:shadow-soft-lg transition-all duration-500">
|
<a href="https://informatika.untan.ac.id/people-based-on-kk.html" class="group block rounded-2xl sm:rounded-2.5xl md:rounded-3xl lg:rounded-3xl xl:rounded-3xl overflow-hidden border border-slate-100 bg-white shadow-soft hover:-translate-y-2 hover:shadow-soft-lg transition-all duration-500">
|
||||||
<div class="p-5 sm:p-6 md:p-7 lg:p-8 xl:p-8 bg-gradient-to-br ${meta.accent} border-b border-slate-100">
|
<div class="p-5 sm:p-6 md:p-7 lg:p-8 xl:p-8 bg-gradient-to-br ${meta.accent} border-b border-slate-100">
|
||||||
<div class="flex items-start justify-between gap-3 sm:gap-4">
|
<div class="flex items-start justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1648,7 +1801,7 @@
|
|||||||
<span class="inline-flex flex-col gap-0.5 text-[8px] sm:text-[8px] md:text-[9px] font-bold uppercase tracking-[0.2em] text-slate-400">
|
<span class="inline-flex flex-col gap-0.5 text-[8px] sm:text-[8px] md:text-[9px] font-bold uppercase tracking-[0.2em] text-slate-400">
|
||||||
${rank.split(' ').join('<br>')}
|
${rank.split(' ').join('<br>')}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center gap-2 text-xs sm:text-xs md:text-sm font-bold text-untan-navy group-hover:text-untan-blue transition-colors whitespace-nowrap">
|
<span class="inline-flex items-center justify-end gap-2 text-right text-xs sm:text-xs md:text-sm font-bold text-untan-navy group-hover:text-untan-blue transition-colors">
|
||||||
${translations[currentLang].viewMembers}
|
${translations[currentLang].viewMembers}
|
||||||
<i class="fa-solid fa-arrow-right transition-transform group-hover:translate-x-1"></i>
|
<i class="fa-solid fa-arrow-right transition-transform group-hover:translate-x-1"></i>
|
||||||
</span>
|
</span>
|
||||||
@@ -1785,14 +1938,17 @@
|
|||||||
revealElements.forEach((element) => observer.observe(element));
|
revealElements.forEach((element) => observer.observe(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setLanguage = function (lang) {
|
document.addEventListener('languageChanged', (e) => {
|
||||||
currentLang = lang;
|
currentLang = e.detail.lang;
|
||||||
updateLangButton();
|
|
||||||
applyTranslation(lang);
|
|
||||||
renderKbkInsight();
|
renderKbkInsight();
|
||||||
renderRisetCards();
|
renderRisetCards();
|
||||||
renderDosenCards();
|
renderDosenCards();
|
||||||
};
|
|
||||||
|
const lastUpdatedEl = document.getElementById("last-updated");
|
||||||
|
if (lastUpdatedEl) {
|
||||||
|
lastUpdatedEl.innerHTML = new Date(document.lastModified).toLocaleString(currentLang === 'id' ? 'id-ID' : 'en-US');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('error', (event) => {
|
window.addEventListener('error', (event) => {
|
||||||
logAppError('window-error', event.error || new Error(event.message));
|
logAppError('window-error', event.error || new Error(event.message));
|
||||||
@@ -1862,12 +2018,20 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
setupRevealAnimations();
|
setupRevealAnimations();
|
||||||
setLanguage(currentLang);
|
if (typeof translations !== 'undefined') {
|
||||||
|
Object.assign(translations.id, rodaTranslations.id);
|
||||||
|
Object.assign(translations.en, rodaTranslations.en);
|
||||||
|
if (typeof applyLang === 'function') applyLang();
|
||||||
|
} else {
|
||||||
|
currentLang = 'id';
|
||||||
|
renderKbkInsight();
|
||||||
|
renderRisetCards();
|
||||||
|
renderDosenCards();
|
||||||
|
}
|
||||||
openKbkInsight(currentKbkInsight, false);
|
openKbkInsight(currentKbkInsight, false);
|
||||||
filterRiset(currentFilter);
|
filterRiset(currentFilter);
|
||||||
searchRiset('');
|
searchRiset('');
|
||||||
fetchDashboardData().catch((error) => {
|
fetchDashboardData().catch((error) => {
|
||||||
logAppError('DOMContentLoaded.fetchDashboardData', error);
|
|
||||||
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(error), 'Coba Lagi', 'fetchDashboardData()');
|
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(error), 'Coba Lagi', 'fetchDashboardData()');
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1991,6 +2155,57 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const stats = [
|
||||||
|
{ id: 'stat-pub', target: 400, suffix: '+', decimals: 0 },
|
||||||
|
{ id: 'stat-sinta', target: 7555, suffix: '', decimals: 0, format: 'dot-thousands' },
|
||||||
|
{ id: 'stat-dosen', target: 33, suffix: '', decimals: 0 },
|
||||||
|
{ id: 'stat-collab', target: 10, suffix: '+', decimals: 0 },
|
||||||
|
{ id: 'stat-mhs', target: 518, suffix: '', decimals: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function easeOutQuart(t) {
|
||||||
|
return 1 - Math.pow(1 - t, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(value, fmt) {
|
||||||
|
if (fmt === 'dot-thousands') {
|
||||||
|
return Math.floor(value).toLocaleString('id-ID');
|
||||||
|
}
|
||||||
|
return Math.floor(value).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateStat(el, target, suffix, fmt, duration) {
|
||||||
|
const start = performance.now();
|
||||||
|
function step(now) {
|
||||||
|
const elapsed = now - start;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const current = easeOutQuart(progress) * target;
|
||||||
|
el.textContent = formatValue(current, fmt) + suffix;
|
||||||
|
if (progress < 1) requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
stats.forEach(function (s) {
|
||||||
|
const el = document.getElementById(s.id);
|
||||||
|
if (el) animateStat(el, s.target, s.suffix, s.format, 1800);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, { threshold: 0.3 });
|
||||||
|
|
||||||
|
const section = document.querySelector('#stat-pub');
|
||||||
|
if (section) observer.observe(section.closest('section'));
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="../../assets/global/localization.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.bg-pattern {
|
.bg-pattern {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +173,11 @@ html {
|
|||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide hamburger button on desktop (aligns with ACAAB lg:1024px) */
|
||||||
|
#mobile-menu-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
#kbk-card-grid {
|
#kbk-card-grid {
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[build]
|
||||||
|
command = "npm run build"
|
||||||
|
publish = "dist"
|
||||||
|
|
||||||
|
[build.environment]
|
||||||
|
NODE_VERSION = "22"
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
import { cpSync, existsSync, readdirSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const excludedDirs = new Set(['.git', 'dist', 'node_modules']);
|
||||||
|
const rodaDashboardUrl = 'https://dashboard.informatika.untan.ac.id/index.php?penelitian';
|
||||||
|
|
||||||
|
let rodaDashboardHtmlCache;
|
||||||
|
|
||||||
|
function collectHtmlInputs(dir) {
|
||||||
|
const inputs = {};
|
||||||
|
const entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (!excludedDirs.has(entry.name)) {
|
||||||
|
Object.assign(inputs, collectHtmlInputs(fullPath));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile() && entry.name.endsWith('.html')) {
|
||||||
|
const relativePath = path.relative(root, fullPath);
|
||||||
|
const inputName = relativePath
|
||||||
|
.replace(/\.html$/, '')
|
||||||
|
.replace(/[^a-zA-Z0-9_-]+/g, '_');
|
||||||
|
|
||||||
|
inputs[inputName] = fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyStaticAssets() {
|
||||||
|
return {
|
||||||
|
name: 'copy-static-assets',
|
||||||
|
closeBundle() {
|
||||||
|
const source = path.join(root, 'assets');
|
||||||
|
const destination = path.join(root, 'dist', 'assets');
|
||||||
|
|
||||||
|
if (existsSync(source)) {
|
||||||
|
cpSync(source, destination, { recursive: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRodaDashboardHtml() {
|
||||||
|
if (rodaDashboardHtmlCache !== undefined) {
|
||||||
|
return rodaDashboardHtmlCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(rodaDashboardUrl, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
rodaDashboardHtmlCache = await response.text();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.warn(`[roda-dashboard] Build-time dashboard fetch skipped: ${message}`);
|
||||||
|
rodaDashboardHtmlCache = '';
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rodaDashboardHtmlCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJsonScriptContent(value) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
.replaceAll('<', '\\u003c')
|
||||||
|
.replaceAll('>', '\\u003e')
|
||||||
|
.replaceAll('&', '\\u0026')
|
||||||
|
.replaceAll('\u2028', '\\u2028')
|
||||||
|
.replaceAll('\u2029', '\\u2029');
|
||||||
|
}
|
||||||
|
|
||||||
|
function embedRodaDashboardData() {
|
||||||
|
let command = 'serve';
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'embed-roda-dashboard-data',
|
||||||
|
configResolved(config) {
|
||||||
|
command = config.command;
|
||||||
|
},
|
||||||
|
async transformIndexHtml(html, context) {
|
||||||
|
const filename = context?.filename ? path.resolve(context.filename) : '';
|
||||||
|
const rodaIndex = path.join(root, 'groups', 'RODA', 'index.html');
|
||||||
|
|
||||||
|
if (filename !== rodaIndex || command !== 'build') {
|
||||||
|
return html.replace('<!-- RODA_DASHBOARD_DATA -->', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardHtml = await fetchRodaDashboardHtml();
|
||||||
|
const payload = `<script type="application/json" id="roda-dashboard-html">${escapeJsonScriptContent(dashboardHtml)}</script>`;
|
||||||
|
|
||||||
|
return html.replace('<!-- RODA_DASHBOARD_DATA -->', payload);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [embedRodaDashboardData(), copyStaticAssets()],
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
input: collectHtmlInputs(root),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user