forked from izu/student-web-if-development-kit
first bro
This commit is contained in:
@@ -80,20 +80,28 @@ body {
|
||||
|
||||
.navbar-logo-name {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
font-size: 1.125rem; /* text-lg = 18px */
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.1em; /* tracking-widest */
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.navbar-logo-sub {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.55);
|
||||
letter-spacing: 1px;
|
||||
margin-top: 2px;
|
||||
font-size: 10px; /* text-[10px] */
|
||||
font-weight: 400;
|
||||
color: rgba(226, 232, 240, 0.9); /* text-slate-200 opacity-90 */
|
||||
letter-spacing: 0.15em; /* tracking-[0.15em] */
|
||||
margin-top: 0.125rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.navbar-logo-name { font-size: 1.5rem; } /* lg:text-2xl = 24px */
|
||||
.navbar-logo-sub { font-size: 0.875rem; letter-spacing: 0.3em; } /* lg:text-sm, lg:tracking-[0.3em] */
|
||||
}
|
||||
|
||||
/* "SATU UNTAN" text */
|
||||
@@ -179,10 +187,14 @@ body {
|
||||
.navbar-main {
|
||||
background: var(--color-secondary);
|
||||
height: 48px;
|
||||
display: flex;
|
||||
display: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.navbar-main { display: flex; }
|
||||
}
|
||||
|
||||
/* Nav links — teks gelap di atas latar kuning */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
@@ -1270,6 +1282,10 @@ body {
|
||||
}
|
||||
|
||||
/* ===== FOOTER ===== */
|
||||
footer {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// =====================================================
|
||||
// API.js — Mekanisme Fetch Data Beasiswa (terpusat)
|
||||
// -----------------------------------------------------
|
||||
// Alur: coba API remote (Directus publik) → jika gagal,
|
||||
// fallback ke berkas lokal `data/beasiswa.json`.
|
||||
//
|
||||
// Data remote dinormalisasi ke skema UI yang sama dengan
|
||||
// data lokal agar filter.js dan popup.js tidak perlu berubah.
|
||||
// =====================================================
|
||||
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
const CONFIG = {
|
||||
baseUrl: 'https://api.ifuntanhub.dev',
|
||||
collection: '/items/beasiswa',
|
||||
localSource: 'data/beasiswa.json',
|
||||
};
|
||||
|
||||
// Normalise status remote → nilai yang dipakai UI
|
||||
function normalizeStatus(s) {
|
||||
if (!s) return 'Ditutup';
|
||||
const lower = s.toLowerCase();
|
||||
if (lower === 'dibuka' || lower === 'buka') return 'Buka';
|
||||
if (lower === 'ditutup') return 'Ditutup';
|
||||
return s; // "Segera Dibuka" atau nilai lain: tampilkan apa adanya
|
||||
}
|
||||
|
||||
// Petakan satu item Directus ke skema yang diharapkan filter.js / popup.js
|
||||
function normalizeItem(item) {
|
||||
const parts = [item.jenjang, item.negara, item.tipe_pendanaan].filter(Boolean);
|
||||
return {
|
||||
id: String(item.id),
|
||||
nama: item.nama || '',
|
||||
status: normalizeStatus(item.status),
|
||||
kategori: item.tipe_pendanaan || '',
|
||||
thumbnail: '',
|
||||
deskripsi_singkat: parts.join(' · ') || 'Info beasiswa tersedia di sumber resmi.',
|
||||
tanggal_buka: item.tanggal_buka || '',
|
||||
tanggal_tutup: item.tenggat_waktu || '',
|
||||
url_sumber: item.url || '',
|
||||
syarat_umum: [],
|
||||
kontak: '',
|
||||
tags: parts,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchRemote() {
|
||||
const res = await fetch(CONFIG.baseUrl + CONFIG.collection);
|
||||
if (!res.ok) throw new Error(`API merespons HTTP ${res.status}`);
|
||||
const json = await res.json();
|
||||
return (json.data || []).map(normalizeItem);
|
||||
}
|
||||
|
||||
async function fetchLocal() {
|
||||
const res = await fetch(CONFIG.localSource);
|
||||
if (!res.ok) throw new Error(`Gagal memuat data lokal (HTTP ${res.status})`);
|
||||
const json = await res.json();
|
||||
return Array.isArray(json) ? json : json.data || [];
|
||||
}
|
||||
|
||||
async function getBeasiswa() {
|
||||
try {
|
||||
const data = await fetchRemote();
|
||||
return { data, source: 'remote' };
|
||||
} catch (err) {
|
||||
console.warn('[BeasiswaAPI] API remote gagal, fallback ke data lokal:', err.message);
|
||||
const data = await fetchLocal();
|
||||
return { data, source: 'local' };
|
||||
}
|
||||
}
|
||||
|
||||
global.BeasiswaAPI = { config: CONFIG, getBeasiswa, fetchRemote, fetchLocal };
|
||||
})(window);
|
||||
@@ -32,7 +32,7 @@ function sanitize(str) {
|
||||
* Gunakan textContent/setAttribute — hindari langsung innerHTML dari user input
|
||||
*/
|
||||
function createCard(bsw) {
|
||||
const isBuka = bsw.status === 'Buka';
|
||||
const isBuka = bsw.status === 'Buka' || bsw.status === 'Dibuka';
|
||||
|
||||
const article = document.createElement('article');
|
||||
article.className = 'scholarship-card fade-in-up';
|
||||
@@ -207,21 +207,25 @@ function initSearch() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch data JSON dan render */
|
||||
/** Fetch data via API.js (BeasiswaAPI) lalu render */
|
||||
async function initDirectory() {
|
||||
const grid = document.getElementById('beasiswa-grid');
|
||||
|
||||
try {
|
||||
const res = await fetch('data/beasiswa.json');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!window.BeasiswaAPI) {
|
||||
throw new Error('Modul API.js (BeasiswaAPI) belum dimuat');
|
||||
}
|
||||
|
||||
allData = await res.json();
|
||||
const { data, source } = await window.BeasiswaAPI.getBeasiswa();
|
||||
allData = data;
|
||||
console.info(`[items] beasiswa dimuat dari sumber: ${source} (${allData.length} item)`);
|
||||
|
||||
// Simpan ke window agar popup.js bisa akses
|
||||
window.beasiswaData = allData;
|
||||
|
||||
renderCards();
|
||||
} catch (err) {
|
||||
console.error('[items] gagal memuat beasiswa', err);
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#fca5a5;"></i>
|
||||
@@ -229,7 +233,7 @@ async function initDirectory() {
|
||||
Gagal memuat data beasiswa
|
||||
</p>
|
||||
<p style="font-size:13px;color:#cbd5e1;">
|
||||
Pastikan menggunakan live server. Silakan refresh atau hubungi admin.
|
||||
Tidak dapat terhubung ke server. Silakan refresh atau hubungi admin.
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -105,24 +105,32 @@ function applyTranslations(lang) {
|
||||
document.documentElement.lang = lang === 'id' ? 'id' : 'en';
|
||||
}
|
||||
|
||||
// Override setLanguage to delegate to localization.js when available,
|
||||
// so both systems stay in sync (localization.js dispatches 'languageChanged'
|
||||
// which triggers our listener below to re-apply dot-key translations).
|
||||
function setLanguage(lang) {
|
||||
if (lang !== 'id' && lang !== 'en') return;
|
||||
currentLang = lang;
|
||||
sessionStorage.setItem('lang', lang);
|
||||
|
||||
// Update button visual states
|
||||
document.querySelectorAll('.lang-btn').forEach(btn => {
|
||||
const isActive = btn.dataset.lang === lang;
|
||||
btn.classList.toggle('active', isActive);
|
||||
});
|
||||
|
||||
applyTranslations(lang);
|
||||
if (typeof setLang === 'function') {
|
||||
setLang(lang); // localization.js handles state + dispatches languageChanged
|
||||
} else {
|
||||
currentLang = lang;
|
||||
sessionStorage.setItem('lang', lang);
|
||||
applyTranslations(lang);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose globally so onclick in HTML works
|
||||
window.setLanguage = setLanguage;
|
||||
|
||||
// Re-apply dot-key translations whenever localization.js changes the language.
|
||||
// This fixes the race where localization.js overwrites dot-key elements with the
|
||||
// raw key string (its fallback for unknown keys).
|
||||
document.addEventListener('languageChanged', (e) => {
|
||||
applyTranslations(e.detail.lang);
|
||||
});
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setLanguage(currentLang);
|
||||
// Use localization.js state if it's already loaded, otherwise fall back.
|
||||
const initLang = (typeof getLang === 'function') ? getLang()
|
||||
: (sessionStorage.getItem('lang') || 'id');
|
||||
applyTranslations(initLang);
|
||||
});
|
||||
|
||||
@@ -55,24 +55,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
|
||||
// ─── Hamburger Menu (Mobile) ─────────────────────────
|
||||
const hamburger = document.getElementById('hamburger');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn = document.getElementById('close-mobile-menu');
|
||||
|
||||
hamburger?.addEventListener('click', () => {
|
||||
const isOpen = !mobileMenu.classList.contains('hidden');
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
const closeMobileMenu = () => {
|
||||
mobileMenu?.classList.add('-translate-x-full');
|
||||
mobileBtn?.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
|
||||
// Toggle icon
|
||||
const icon = hamburger.querySelector('i');
|
||||
if (isOpen) {
|
||||
icon.className = 'fas fa-bars text-lg';
|
||||
} else {
|
||||
icon.className = 'fas fa-times text-lg';
|
||||
}
|
||||
|
||||
// Aksesibilitas
|
||||
hamburger.setAttribute('aria-expanded', String(!isOpen));
|
||||
});
|
||||
// openMobileMenu handled by inline script in HTML; this covers close-on-scroll
|
||||
closeMobileBtn?.addEventListener('click', closeMobileMenu);
|
||||
|
||||
|
||||
// ─── Smooth Scroll untuk anchor links ──────────────
|
||||
@@ -90,9 +83,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
|
||||
// Tutup mobile menu jika sedang terbuka
|
||||
mobileMenu?.classList.add('hidden');
|
||||
const icon = hamburger?.querySelector('i');
|
||||
if (icon) icon.className = 'fas fa-bars text-lg';
|
||||
closeMobileMenu();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function openPopup(id) {
|
||||
const bsw = data.find(b => b.id === id);
|
||||
if (!bsw) return;
|
||||
|
||||
const isBuka = bsw.status === 'Buka';
|
||||
const isBuka = bsw.status === 'Buka' || bsw.status === 'Dibuka';
|
||||
const badgeClass = isBuka ? 'badge-buka' : 'badge-ditutup';
|
||||
|
||||
// Bangun list syarat umum
|
||||
|
||||
+1657
-569
File diff suppressed because it is too large
Load Diff
@@ -178,6 +178,9 @@
|
||||
"deadline": "2026-09-25",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/deakin-university-vice-chancellor-s-international-scholarship-10"
|
||||
},
|
||||
{
|
||||
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
Reference in New Issue
Block a user