first bro
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
groups/**/.env
|
||||
groups/**/.env.local
|
||||
|
||||
@@ -569,6 +569,33 @@ const translations = {
|
||||
kontrib_guideline_3: "Sertakan fakta dan data yang akurat dan dapat diverifikasi.",
|
||||
kontrib_guideline_4: "Hindari plagiarisme. Karya harus orisinal.",
|
||||
kontrib_guideline_5: "Tim redaksi berhak menyunting konten untuk keperluan editorial.",
|
||||
|
||||
// --- apeNameTeamE (Halaman Beasiswa) ---
|
||||
ape_breadcrumb_profile: 'Profil',
|
||||
ape_breadcrumb_scholarship: 'Beasiswa',
|
||||
ape_hero_badge: 'Program Beasiswa',
|
||||
ape_hero_title: 'Beasiswa Studi &<br>Penelitian',
|
||||
ape_hero_subtitle: 'Teknik Informatika Universitas Tanjungpura',
|
||||
ape_hero_desc: 'Temukan berbagai peluang beasiswa untuk mendukung perjalanan akademik dan penelitian Anda. Program Studi Informatika UNTAN berkomitmen membantu mahasiswa meraih prestasi terbaik.',
|
||||
ape_hero_cta: 'Lihat Daftar Beasiswa',
|
||||
ape_stats_total: 'Total Mahasiswa Penerima<br>Beasiswa Semester Ini',
|
||||
ape_stats_gov: 'Total Penerima Beasiswa<br>Pembiayaan Pemerintah',
|
||||
ape_stats_private: 'Total Penerima Beasiswa<br>Pembiayaan Swasta',
|
||||
ape_dir_label: 'Daftar Beasiswa',
|
||||
ape_dir_title: 'Daftar Beasiswa',
|
||||
ape_dir_desc: 'Temukan beragam beasiswa dari berbagai sumber resmi yang tersedia untuk mahasiswa aktif, alumni, dan calon mahasiswa Program Studi Informatika UNTAN.',
|
||||
ape_dir_guide: 'Terima Bantuan Beasiswa?',
|
||||
ape_filter_open: 'Sedang Buka',
|
||||
ape_filter_closed: 'Ditutup',
|
||||
ape_filter_gov: 'Pemerintah',
|
||||
ape_filter_private: 'Swasta',
|
||||
ape_search_placeholder: 'Cari Nama Beasiswa...',
|
||||
ape_testimonial_title: 'Apa Kata Mereka?',
|
||||
ape_testimonial_desc: 'Cerita inspiratif dari para penerima beasiswa Program Studi Informatika UNTAN',
|
||||
ape_cta_title: 'Ingin Mengetahui Info Lebih Lanjut?',
|
||||
ape_cta_desc: 'Hubungi admin prodi atau ikuti Instagram kemahasiswaan UNTAN untuk informasi terbaru tentang beasiswa dan program akademik.',
|
||||
ape_cta_btn1: 'DAFTAR BEASISWA',
|
||||
ape_cta_btn2: 'Hubungi Kami',
|
||||
},
|
||||
|
||||
en: {
|
||||
@@ -1135,6 +1162,33 @@ const translations = {
|
||||
kontrib_guideline_3: "Include accurate and verifiable facts and data.",
|
||||
kontrib_guideline_4: "Avoid plagiarism. Work must be original.",
|
||||
kontrib_guideline_5: "The editorial team reserves the right to edit content for editorial purposes.",
|
||||
|
||||
// --- apeNameTeamE (Scholarship Page) ---
|
||||
ape_breadcrumb_profile: 'Profile',
|
||||
ape_breadcrumb_scholarship: 'Scholarship',
|
||||
ape_hero_badge: 'Scholarship Program',
|
||||
ape_hero_title: 'Study & Research<br>Scholarships',
|
||||
ape_hero_subtitle: 'Informatics Engineering, Tanjungpura University',
|
||||
ape_hero_desc: 'Discover various scholarship opportunities to support your academic and research journey. The Informatics Study Program at UNTAN is committed to helping students achieve excellence.',
|
||||
ape_hero_cta: 'View Scholarships',
|
||||
ape_stats_total: 'Total Scholarship<br>Recipients This Semester',
|
||||
ape_stats_gov: 'Total Government<br>Scholarship Recipients',
|
||||
ape_stats_private: 'Total Private<br>Scholarship Recipients',
|
||||
ape_dir_label: 'Scholarship Directory',
|
||||
ape_dir_title: 'Scholarship Directory',
|
||||
ape_dir_desc: 'Find various scholarships from official sources available for active students, alumni, and prospective students of the UNTAN Informatics Study Program.',
|
||||
ape_dir_guide: 'Need Scholarship Aid?',
|
||||
ape_filter_open: 'Open',
|
||||
ape_filter_closed: 'Closed',
|
||||
ape_filter_gov: 'Government',
|
||||
ape_filter_private: 'Private',
|
||||
ape_search_placeholder: 'Search Scholarship Name...',
|
||||
ape_testimonial_title: 'What They Say?',
|
||||
ape_testimonial_desc: 'Inspiring stories from scholarship recipients at UNTAN Informatics Study Program',
|
||||
ape_cta_title: 'Want to Know More?',
|
||||
ape_cta_desc: 'Contact the program admin or follow UNTAN student affairs Instagram for the latest scholarship and academic program information.',
|
||||
ape_cta_btn1: 'SCHOLARSHIP LIST',
|
||||
ape_cta_btn2: 'Contact Us',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -997,7 +997,7 @@
|
||||
|
||||
<!-- Overlay for mega menu -->
|
||||
<div id="mega-overlay" class="fixed inset-0 bg-black/30 backdrop-blur-sm z-30"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu"
|
||||
class="fixed inset-0 bg-slate-900 z-[60] transform -translate-x-full lg:hidden flex flex-col transition-transform duration-300"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Generated
+7
-8
@@ -886,9 +886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -917,7 +917,6 @@
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -926,9 +925,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -946,7 +945,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user