Feat & Fix: Optimasi UI Mobile, Animasi Statistik, dan Dashboard Proxy #4

Merged
Tazyeuu merged 24 commits from feat-fix-mobile-dashboard-anim-hamburger into prod 2026-06-04 17:18:47 +00:00
8 changed files with 2320 additions and 1422 deletions
+10
View File
@@ -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 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
```
+1959 -1280
View File
File diff suppressed because it is too large Load Diff
+190 -117
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="id">
<head>
@@ -74,6 +74,8 @@
</script>
<script>
window.APP_CONFIG = window.APP_CONFIG || {
debugLogs: false,
dashboardRuntimeFetch: 'local',
dashboardTargetUrl: 'https://dashboard.informatika.untan.ac.id/index.php?penelitian',
dashboardProxyTemplates: [
'https://api.allorigins.win/get?url={{url}}&ts={{ts}}',
@@ -82,6 +84,7 @@
]
};
</script>
<!-- RODA_DASHBOARD_DATA -->
<link rel="stylesheet" href="style.css">
</head>
@@ -130,9 +133,8 @@
class="hover:text-untan-yellow transition-colors font-medium">SATU UNTAN</a>
</div>
<div class="pl-5 border-l border-white/20 flex items-center gap-3">
<button id="lang-toggle-btn"
class="language-toggle-btn text-xs hover:text-untan-yellow transition-colors flex items-center gap-1"
onclick="setLanguage(currentLang === 'id' ? 'en' : 'id')">
<button
class="language-toggle-btn text-xs hover:text-untan-yellow transition-colors flex items-center gap-1">
<span class="font-bold">ID</span> <span class="text-white/40">|</span> EN
</button>
<button
@@ -436,10 +438,9 @@
role="dialog" aria-modal="true" aria-label="Menu navigasi mobile">
<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-serif">Menu Utama</span>
<button id="lang-toggle-mobile"
<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"
onclick="setLanguage(currentLang === 'id' ? 'en' : 'id')"
aria-label="Ganti bahasa">
<span class="font-bold">ID</span> <span class="text-white/40" aria-hidden="true">|</span> EN
</button>
@@ -462,7 +463,7 @@
<!-- Tentang Kami -->
<div class="pt-2">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_about">Tentang
<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"
@@ -478,7 +479,7 @@
<!-- Program -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
<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"
@@ -498,7 +499,7 @@
<!-- SDM -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_sdm">SDM</span>
<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"
@@ -516,7 +517,7 @@
<!-- Fasilitas -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
<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"
@@ -532,7 +533,7 @@
<!-- Karya & Agenda -->
<div class="pt-2 border-t border-slate-200">
<span class="text-xs font-bold text-slate-400 uppercase font-serif" data-i18n="nav_works">Karya &
<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"
@@ -549,7 +550,7 @@
<!-- Kontak -->
<div class="pt-2 border-t border-slate-200 pb-8">
<span class="text-xs font-bold text-slate-400 uppercase font-serif"
<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"
@@ -611,7 +612,7 @@
topik riset spesifik, dipimpin dosen berpengalaman, dan terbuka untuk kolaborasi.</p>
</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 -->
<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">
@@ -928,7 +929,7 @@
</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 -->
</div>
@@ -955,7 +956,7 @@
ketua setiap KBK. Pilih kartu untuk melihat dosen lain di kelompok keahlian tersebut.</p>
</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 -->
</div>
</div>
@@ -1325,7 +1326,7 @@
}
};
const translations = {
const rodaTranslations = {
id: {
info: 'INFO TERBARU',
tickerItem1: 'Selamat datang di antarmuka situs web Informatika terbaru!',
@@ -1446,7 +1447,7 @@
}
};
let currentLang = 'id';
let currentFilter = 'all';
let currentQuery = '';
let currentKbkInsight = 'ai';
@@ -1471,6 +1472,7 @@
}
function logAppError(scope, error, extra = {}) {
if (window.APP_CONFIG?.debugLogs !== true) return;
console.error(`[${scope}]`, error, extra);
}
@@ -1481,6 +1483,8 @@
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 === '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 ')}).`;
return 'Gagal mengambil data. Server dashboard mungkin sedang sibuk atau memblokir akses.';
}
@@ -1541,6 +1545,80 @@
return rows;
}
function shouldUseLocalDashboardProxy() {
const hostname = window.location.hostname;
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '';
}
function isDashboardRuntimeFetchAllowed() {
const mode = window.APP_CONFIG?.dashboardRuntimeFetch;
return mode === true || mode === 'always' || (mode === 'local' && shouldUseLocalDashboardProxy());
}
function readEmbeddedDashboardHtml() {
const source = document.getElementById('roda-dashboard-html');
if (!source) return '';
try {
return JSON.parse(source.textContent || '""') || '';
} catch (error) {
return source.textContent || '';
}
}
function applyDashboardRows(rows) {
const newData = [];
rows.forEach((row) => {
const cols = row.querySelectorAll('td');
if (cols.length >= 3) {
const indexVal = cols[0].innerText.trim();
// Mencari elemen judul (biasanya di dalam <a> atau <b>)
const linkEl = cols[1].querySelector('a');
const titleEl = linkEl || cols[1].querySelector('b') || cols[1];
const url = linkEl ? linkEl.getAttribute('href') : '#';
const title = titleEl.innerText.trim();
// Skip jika baris ini adalah header atau bukan data penelitian (berdasarkan index atau judul)
if (isNaN(parseInt(indexVal)) || title === "Bidang Riset" || title === "Thn Jlh") {
return;
}
// Mencari elemen penulis (biasanya di dalam tag <font> dengan size 11px)
const authorsEl = cols[1].querySelector('font');
const authors = authorsEl ? authorsEl.innerText.trim() : 'Peneliti Informatika';
// Tahun biasanya ada di kolom berikutnya
const year = cols[2]?.innerText.trim() || '2025';
const kbk = categorizeByKeywords(title);
const kbkInfo = kbkMeta[kbk];
newData.push({
id: parseInt(indexVal) || (newData.length + 1),
kbk: kbk,
title_id: title,
title_en: title,
authors: authors,
year: year,
url: url,
venue: 'Dashboard Informatika',
badge_id: kbkInfo.label_id,
badge_en: kbkInfo.label_en
});
}
});
if (!newData.length) {
throw new Error('NO_ROWS_FOUND');
}
risetData = newData;
isLoadingData = false;
renderRisetCards();
updateStatPub(newData.length);
}
async function fetchDashboardData() {
const grid = document.getElementById('riset-grid');
if (grid) {
@@ -1552,15 +1630,31 @@
`;
}
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)
: [];
// Sumber utama: endpoint proxy lokal (server.js fetch server-side, tanpa CORS).
const proxyServers = [() => '/api/dashboard'];
// Endpoint server.js hanya ada saat development lokal.
const proxyServers = shouldUseLocalDashboardProxy() ? [() => '/api/dashboard'] : [];
// Fallback: proxy publik (butuh targetUrl untuk membangun URL).
// Deployment static memakai proxy publik karena tidak menjalankan server.js.
if (targetUrl) {
proxyTemplates.forEach((template) => {
proxyServers.push((url) => template
@@ -1570,7 +1664,10 @@
});
}
let lastError = null;
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();
@@ -1578,7 +1675,6 @@
try {
const proxyUrl = getProxyUrl(targetUrl);
console.log(`Trying proxy: ${proxyUrl}`);
const response = await fetch(proxyUrl, { signal: controller.signal });
clearTimeout(timeoutId);
@@ -1588,101 +1684,21 @@
const responseData = await response.text();
const html = extractDashboardHtml(responseData);
const rows = parseDashboardRows(html);
const newData = [];
rows.forEach((row, index) => {
const cols = row.querySelectorAll('td');
if (cols.length >= 3) {
const indexVal = cols[0].innerText.trim();
// Mencari elemen judul (biasanya di dalam <a> atau <b>)
const linkEl = cols[1].querySelector('a');
const titleEl = linkEl || cols[1].querySelector('b') || cols[1];
const url = linkEl ? linkEl.getAttribute('href') : '#';
const title = titleEl.innerText.trim();
// Skip jika baris ini adalah header atau bukan data penelitian (berdasarkan index atau judul)
if (isNaN(parseInt(indexVal)) || title === "Bidang Riset" || title === "Thn Jlh") {
return;
}
// Mencari elemen penulis (biasanya di dalam tag <font> dengan size 11px)
const authorsEl = cols[1].querySelector('font');
const authors = authorsEl ? authorsEl.innerText.trim() : 'Peneliti Informatika';
// Tahun biasanya ada di kolom berikutnya
const year = cols[2]?.innerText.trim() || '2025';
const kbk = categorizeByKeywords(title);
const kbkInfo = kbkMeta[kbk];
newData.push({
id: parseInt(indexVal) || (newData.length + 1),
kbk: kbk,
title_id: title,
title_en: title,
authors: authors,
year: year,
url: url,
venue: 'Dashboard Informatika',
badge_id: kbkInfo.label_id,
badge_en: kbkInfo.label_en
});
}
});
if (newData.length > 0) {
risetData = newData;
isLoadingData = false;
renderRisetCards();
updateStatPub(newData.length);
return; // Berhasil! Keluar dari loop
}
throw new Error('NO_ROWS_FOUND');
applyDashboardRows(rows);
return; // Berhasil! Keluar dari loop
} catch (error) {
clearTimeout(timeoutId);
logAppError('dashboard-proxy', error, { targetUrl });
lastError = error;
// Lanjut ke proxy berikutnya...
}
}
// Jika semua proxy gagal
logAppError('fetchDashboardData', lastError);
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(lastError), 'Coba Lagi', 'fetchDashboardData()');
}
function updateLangButton() {
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('lang-toggle-mobile');
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]);
}
});
}
// Global localization.js handles translations and language toggle
function matchesQuery(item, query) {
const haystack = [item.title_id, item.title_en, item.authors, item.venue, item.year].join(' ').toLowerCase();
@@ -1728,9 +1744,9 @@
}
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">
<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>
<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>
@@ -1785,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">
${rank.split(' ').join('<br>')}
</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}
<i class="fa-solid fa-arrow-right transition-transform group-hover:translate-x-1"></i>
</span>
@@ -1922,19 +1938,17 @@
revealElements.forEach((element) => observer.observe(element));
}
window.setLanguage = function (lang) {
currentLang = lang;
updateLangButton();
applyTranslation(lang);
document.addEventListener('languageChanged', (e) => {
currentLang = e.detail.lang;
renderKbkInsight();
renderRisetCards();
renderDosenCards();
const lastUpdatedEl = document.getElementById("last-updated");
if (lastUpdatedEl) {
lastUpdatedEl.innerHTML = new Date(document.lastModified).toLocaleString(lang === 'id' ? 'id-ID' : 'en-US');
lastUpdatedEl.innerHTML = new Date(document.lastModified).toLocaleString(currentLang === 'id' ? 'id-ID' : 'en-US');
}
};
});
window.addEventListener('error', (event) => {
logAppError('window-error', event.error || new Error(event.message));
@@ -2004,12 +2018,20 @@
});
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);
filterRiset(currentFilter);
searchRiset('');
fetchDashboardData().catch((error) => {
logAppError('DOMContentLoaded.fetchDashboardData', error);
renderRisetFeedback('Pemuatan Data Gagal', getDashboardErrorMessage(error), 'Coba Lagi', 'fetchDashboardData()');
});
} catch (error) {
@@ -2133,6 +2155,57 @@
}
})();
</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>
</html>
+11 -11
View File
@@ -1,13 +1,13 @@
{
"name": "roda",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roda",
"version": "1.0.0",
"license": "ISC"
}
"name": "roda",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roda",
"version": "1.0.0",
"license": "ISC"
}
}
}
}
+13 -13
View File
@@ -1,14 +1,14 @@
{
"name": "roda",
"version": "1.0.0",
"description": "Project website untuk menampilkan sorotan dan prestasi karya mahasiswa program studi Informatika Universitas Tanjungpura.",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
"name": "roda",
"version": "1.0.0",
"description": "Project website untuk menampilkan sorotan dan prestasi karya mahasiswa program studi Informatika Universitas Tanjungpura.",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
+6 -1
View File
@@ -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");
}
@@ -173,6 +173,11 @@ html {
display: flex !important;
}
/* Hide hamburger button on desktop (aligns with ACAAB lg:1024px) */
#mobile-menu-button {
display: none !important;
}
#kbk-card-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
+6
View File
@@ -0,0 +1,6 @@
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "22"
+125
View File
@@ -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),
},
},
});