Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a4fa612e9 |
@@ -1,9 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
groups/**/.env
|
||||
groups/**/.env.local
|
||||
README.md
|
||||
i18n.js
|
||||
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ACAAB — Dev Preview</title>
|
||||
<style>
|
||||
#dev-banner {
|
||||
background: #feb401; color: #003150; font-size: 11px; font-weight: 700;
|
||||
text-align: center; padding: 6px; letter-spacing: .05em; position: sticky; top: 0; z-index: 999;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="../acaab.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="../acaab.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="dev-banner">⚡ DEV MODE — Navbar & footer tidak tampil di sini. Gunakan kit dev server (root) untuk preview lengkap.</div>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
@@ -1,6 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" class="scroll-smooth">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -8,12 +7,9 @@
|
||||
<link rel="stylesheet" href="../../assets/global/output.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<style>
|
||||
<style>
|
||||
/* ===== MOBILE NAV STABILITY FIX =====
|
||||
Prevents logo bar from collapsing/shifting on Android when
|
||||
address bar hides/shows and triggers viewport reflow.
|
||||
@@ -297,7 +293,6 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-slate-800 antialiased bg-white selection:bg-untan-yellow selection:text-black">
|
||||
|
||||
<a href="#main-content" class="skip-link" data-i18n="skip_to_content">Langsung ke Konten Utama</a>
|
||||
@@ -1002,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"
|
||||
@@ -1144,341 +1139,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<div class="container mx-auto px-4 lg:px-8 py-12">
|
||||
<div class="mb-8 pb-8 border-b border-slate-200">
|
||||
<span
|
||||
class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas
|
||||
Halaman Statis</span>
|
||||
<span class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas Halaman Statis</span>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Sorotan dan Prestasi Karya Mahasiswa</h1>
|
||||
<p class="text-slate-500 text-sm">Kelompok 06 — ACAAB · Informatika UNTAN 2026
|
||||
</p>
|
||||
<p class="text-slate-500 text-sm">Kelompok 06 — ACAAB · Informatika UNTAN 2026</p>
|
||||
</div>
|
||||
<div id="assignment-body" class="prose prose-slate max-w-none">
|
||||
<!-- ================= START: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
|
||||
<!-- ── BREADCRUMB ─────────────────────────────────────────────────────── -->
|
||||
<nav class="not-prose py-3" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center gap-1 text-[10px] text-slate-500 font-medium tracking-wide uppercase">
|
||||
<li><a href="https://informatika.untan.ac.id" target="_blank" rel="noopener noreferrer"
|
||||
class="hover:text-blue-600 transition-colors">Beranda</a></li>
|
||||
<li><span class="mx-1 text-slate-300" aria-hidden="true">/</span></li>
|
||||
<li><span class="text-slate-800 font-bold" aria-current="page"
|
||||
data-i18n="grp_acaab_hero_badge">Mahasiswa & Prestasi</span></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- ── HERO ───────────────────────────────────────────────────────────── -->
|
||||
<section id="acaab-hero"
|
||||
class="not-prose acaab-full-bleed bg-untan-navy text-white relative overflow-hidden"
|
||||
style="padding-top:3rem;padding-bottom:0;margin-top:0.5rem;">
|
||||
<!-- Decorative glow -->
|
||||
<div class="absolute top-0 right-0 w-80 h-80 rounded-full pointer-events-none"
|
||||
style="background:rgba(254,180,1,0.08);filter:blur(80px);margin-right:-5rem;margin-top:-5rem;">
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 lg:px-8">
|
||||
<div style="display:grid;grid-template-columns:1fr;gap:1.5rem;align-items:center;"
|
||||
class="lg-hero-grid">
|
||||
<!-- Left: text -->
|
||||
<div style="padding:1.5rem 0 2rem;max-width:640px;">
|
||||
<span class="acaab-fade-in"
|
||||
style="display:inline-block;background:#feb401;color:#0f172a;font-size:0.65rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;padding:0.2rem 0.625rem;margin-bottom:0.75rem;"
|
||||
data-i18n="grp_acaab_hero_badge">MAHASISWA & PRESTASI</span>
|
||||
<h2 class="acaab-fade-in"
|
||||
style="font-size:clamp(1.875rem,3.5vw,2.5rem);font-weight:700;line-height:1.2;letter-spacing:-0.01em;margin:0 0 0.75rem;font-family:'Roboto Condensed',sans-serif;"
|
||||
data-i18n="grp_acaab_hero_title">Sorotan dan Prestasi Karya Mahasiswa</h2>
|
||||
<p class="acaab-fade-in"
|
||||
style="color:rgba(255,255,255,0.7);font-size:0.9375rem;line-height:1.7;margin:0;"
|
||||
data-i18n="grp_acaab_hero_desc">Temukan pencapaian luar biasa mahasiswa Informatika
|
||||
UNTAN.</p>
|
||||
</div>
|
||||
<!-- Right: image -->
|
||||
<div id="acaab-hero-img-wrap"
|
||||
style="display:flex;justify-content:flex-end;align-items:flex-end;position:relative;">
|
||||
<div
|
||||
style="position:absolute;bottom:0;width:280px;height:280px;background:rgba(103,232,249,0.06);border-radius:50%;filter:blur(60px);">
|
||||
</div>
|
||||
<div style="position:relative;">
|
||||
<img src="./hero.png" alt="Mahasiswa Informatika UNTAN"
|
||||
width="612" height="407"
|
||||
fetchpriority="high"
|
||||
style="position:relative;z-index:10;width:100%;max-width:320px;height:auto;object-fit:contain;opacity:0.95;filter:drop-shadow(0 15px 35px rgba(0,0,0,0.25));transition:transform 0.5s;"
|
||||
onerror="this.style.display='none'">
|
||||
<!-- Gradient fades over image -->
|
||||
<div
|
||||
style="position:absolute;bottom:0;left:0;width:100%;height:8rem;background:linear-gradient(to top,#003150,transparent);z-index:20;pointer-events:none;">
|
||||
</div>
|
||||
<div
|
||||
style="position:absolute;top:0;left:0;height:100%;width:4rem;background:linear-gradient(to right,#003150,transparent);z-index:20;pointer-events:none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive: 2-column on desktop -->
|
||||
<style>
|
||||
@media (min-width:1024px) {
|
||||
.lg-hero-grid {
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
}
|
||||
|
||||
#acaab-hero-img-wrap img {
|
||||
max-width: 420px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</section>
|
||||
|
||||
<!-- ── STATS (yellow card) ────────────────────────────────────────────── -->
|
||||
<div id="acaab-stats" class="not-prose" style="padding:1.5rem 0 0;">
|
||||
<div
|
||||
style="background:#feb401;border-radius:0.75rem;box-shadow:0 10px 30px -10px rgba(253,184,19,0.4);padding:1.25rem 1.5rem 1.5rem;">
|
||||
|
||||
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
|
||||
<!-- Prestasi Block -->
|
||||
<div style="flex:4;min-width:240px;">
|
||||
<div style="background:#003150;border-radius:0.5rem;padding:0.5rem 0.75rem;text-align:center;margin-bottom:1.25rem;">
|
||||
<span style="color:white;font-size:0.875rem;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;"
|
||||
data-i18n="grp_acaab_stats_total_ach">Total Prestasi</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:0.25rem;">
|
||||
<!-- Prestasi Mahasiswa (total) -->
|
||||
<div style="flex:1;text-align:center;padding:0 0.15rem;">
|
||||
<div id="stat-total-ach"
|
||||
style="font-size:clamp(2rem,4.5vw,4rem);font-weight:900;color:#003150;line-height:1;font-family:'Roboto Condensed',sans-serif;">
|
||||
0</div>
|
||||
<div style="font-size:clamp(0.5rem,1.2vw,0.85rem);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;color:#003150;margin-top:0.5rem;line-height:1.3;word-break:break-word;"
|
||||
data-i18n="grp_acaab_stats_label_ach">Prestasi<br>Mahasiswa</div>
|
||||
</div>
|
||||
<!-- Internasional -->
|
||||
<div style="flex:1;text-align:center;padding:0 0.15rem;">
|
||||
<div id="stat-intl"
|
||||
style="font-size:clamp(2rem,4.5vw,4rem);font-weight:900;color:#003150;line-height:1;font-family:'Roboto Condensed',sans-serif;">
|
||||
0</div>
|
||||
<div style="font-size:clamp(0.5rem,1.2vw,0.85rem);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;color:#003150;margin-top:0.5rem;word-break:break-word;"
|
||||
data-i18n="grp_acaab_stats_intl">Internasional</div>
|
||||
</div>
|
||||
<!-- Nasional -->
|
||||
<div style="flex:1;text-align:center;padding:0 0.15rem;">
|
||||
<div id="stat-national"
|
||||
style="font-size:clamp(2rem,4.5vw,4rem);font-weight:900;color:#003150;line-height:1;font-family:'Roboto Condensed',sans-serif;">
|
||||
0</div>
|
||||
<div style="font-size:clamp(0.5rem,1.2vw,0.85rem);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;color:#003150;margin-top:0.5rem;word-break:break-word;"
|
||||
data-i18n="grp_acaab_stats_national">Nasional</div>
|
||||
</div>
|
||||
<!-- Regional -->
|
||||
<div style="flex:1;text-align:center;padding:0 0.15rem;">
|
||||
<div id="stat-regional"
|
||||
style="font-size:clamp(2rem,4.5vw,4rem);font-weight:900;color:#003150;line-height:1;font-family:'Roboto Condensed',sans-serif;">
|
||||
0</div>
|
||||
<div style="font-size:clamp(0.5rem,1.2vw,0.85rem);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;color:#003150;margin-top:0.5rem;word-break:break-word;"
|
||||
data-i18n="grp_acaab_stats_regional">Regional</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proyek Block -->
|
||||
<div style="flex:1;min-width:120px;">
|
||||
<div style="background:#003150;border-radius:0.5rem;padding:0.5rem 0.75rem;text-align:center;margin-bottom:1.25rem;">
|
||||
<span style="color:white;font-size:0.875rem;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;"
|
||||
data-i18n="grp_acaab_stats_total_proj">Total Portofolio</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:center;">
|
||||
<!-- Proyek Mahasiswa (total) -->
|
||||
<div style="text-align:center;padding:0 0.15rem;">
|
||||
<div id="stat-total-proj"
|
||||
style="font-size:clamp(2rem,4.5vw,4rem);font-weight:900;color:#003150;line-height:1;font-family:'Roboto Condensed',sans-serif;">
|
||||
0</div>
|
||||
<div style="font-size:clamp(0.5rem,1.2vw,0.85rem);font-weight:800;text-transform:uppercase;letter-spacing:0.08em;color:#003150;margin-top:0.5rem;line-height:1.3;word-break:break-word;"
|
||||
data-i18n="grp_acaab_stats_label_proj">Proyek<br>Mahasiswa</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Data note -->
|
||||
<p id="acaab-data-note"
|
||||
style="font-size:0.6rem;font-weight:600;color:#94a3b8;letter-spacing:0.05em;text-align:right;padding-right:0.25rem;margin:0.5rem 0 0;">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ── ACHIEVEMENT GRID ────────────────────────────────────────────────── -->
|
||||
<section id="acaab-ach-section" class="not-prose" style="padding:2.5rem 0 3rem;">
|
||||
<div style="margin-bottom:2rem;">
|
||||
<h2 style="font-size:clamp(1.5rem,3vw,2.25rem);font-weight:900;color:#003150;margin:0 0 0.5rem;font-family:'Roboto Condensed',sans-serif;"
|
||||
data-i18n="grp_acaab_ach_title">Prestasi Mahasiswa</h2>
|
||||
<p style="color:#64748b;font-size:1rem;max-width:42rem;margin:0;line-height:1.6;"
|
||||
data-i18n="grp_acaab_ach_desc">Daftar pencapaian dan penghargaan yang diraih oleh mahasiswa Informatika UNTAN di berbagai kompetisi.</p>
|
||||
</div>
|
||||
|
||||
<!-- Filter panel — gold top border matching original -->
|
||||
<div id="acaab-ach-filters"
|
||||
style="background:rgba(255,255,255,0.8);backdrop-filter:blur(12px);padding:1rem;border-radius:0.5rem;box-shadow:0 1px 3px rgba(0,0,0,0.08);border-top:4px solid #feb401;margin-bottom:2rem;">
|
||||
<!-- Row 1: Label + Reset -->
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<div
|
||||
style="display:flex;align-items:center;gap:0.5rem;font-size:0.875rem;font-weight:700;color:#003150;font-family:'Roboto Condensed',sans-serif;">
|
||||
<i class="fas fa-filter" style="font-size:0.8rem;"></i>
|
||||
<span>Filter Prestasi</span>
|
||||
</div>
|
||||
<button id="acaab-ach-reset"
|
||||
style="display:none;align-items:center;gap:0.25rem;font-size:0.75rem;font-weight:700;color:#64748b;background:none;border:none;cursor:pointer;"
|
||||
data-i18n="btn_reset_filter">
|
||||
<i class="fas fa-times" style="font-size:0.7rem;"></i> Reset Filter
|
||||
</button>
|
||||
</div>
|
||||
<!-- Row 2: Search -->
|
||||
<div style="position:relative;margin-bottom:0.75rem;">
|
||||
<i class="fas fa-search"
|
||||
style="position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:#94a3b8;font-size:0.8rem;pointer-events:none;"></i>
|
||||
<input id="acaab-ach-search" type="search"
|
||||
style="width:100%;background:#f8fafc;color:#334155;font-size:0.875rem;border-radius:0.375rem;padding:0.5rem 0.75rem 0.5rem 2.25rem;border:1px solid #e2e8f0;outline:none;box-sizing:border-box;"
|
||||
placeholder="Cari prestasi..." data-i18n-placeholder="grp_acaab_ach_search">
|
||||
</div>
|
||||
<!-- Row 3: Dropdowns -->
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.625rem;">
|
||||
<select id="acaab-ach-level" aria-label="Filter tingkat prestasi"
|
||||
style="flex:1;min-width:130px;background:#f8fafc;color:#334155;font-size:0.8rem;font-weight:700;border-radius:0.375rem;padding:0.5rem 0.75rem;border:1px solid #e2e8f0;cursor:pointer;"></select>
|
||||
<select id="acaab-ach-cat" aria-label="Filter kategori prestasi"
|
||||
style="flex:1;min-width:130px;background:#f8fafc;color:#334155;font-size:0.8rem;font-weight:700;border-radius:0.375rem;padding:0.5rem 0.75rem;border:1px solid #e2e8f0;cursor:pointer;"></select>
|
||||
<select id="acaab-ach-year" aria-label="Filter tahun prestasi"
|
||||
style="flex:1;min-width:120px;background:#f8fafc;color:#334155;font-size:0.8rem;font-weight:700;border-radius:0.375rem;padding:0.5rem 0.75rem;border:1px solid #e2e8f0;cursor:pointer;"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result count -->
|
||||
<p id="acaab-ach-count" style="font-size:0.8rem;color:#64748b;margin-bottom:1rem;"></p>
|
||||
|
||||
<!-- Cards grid -->
|
||||
<div id="acaab-ach-grid"
|
||||
style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:2rem;justify-content:center;">
|
||||
</div>
|
||||
|
||||
<!-- Pagination buttons -->
|
||||
<div style="text-align:center;margin-top:2rem;display:flex;gap:0.75rem;justify-content:center;">
|
||||
<button id="acaab-ach-more"
|
||||
style="display:none;padding:0.625rem 1.5rem;background:#003150;color:white;border:none;border-radius:0.5rem;font-weight:700;font-size:0.875rem;cursor:pointer;"
|
||||
data-i18n="grp_acaab_ach_load_more">Tampilkan Lebih Banyak</button>
|
||||
<button id="acaab-ach-less"
|
||||
style="display:none;padding:0.625rem 1.5rem;background:white;color:#003150;border:1px solid #003150;border-radius:0.5rem;font-weight:700;font-size:0.875rem;cursor:pointer;"
|
||||
data-i18n="grp_acaab_ach_show_less">Tampilkan Lebih Sedikit</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── PROJECT CAROUSEL ───────────────────────────────────────────────── -->
|
||||
<section id="acaab-proj-section" class="not-prose acaab-full-bleed" style="padding:2.5rem 0 3rem;background:#f8fafc;">
|
||||
<div class="container mx-auto px-4 lg:px-8">
|
||||
<!-- Section header row: title + scroll buttons (seperti di React source) -->
|
||||
<div
|
||||
style="display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:1rem;margin-bottom:1.5rem;">
|
||||
<div style="flex:1;min-width:250px;">
|
||||
<h2 style="font-size:clamp(1.5rem,3vw,2.25rem);font-weight:900;color:#003150;margin:0 0 0.5rem;font-family:'Roboto Condensed',sans-serif;"
|
||||
data-i18n="grp_acaab_proj_title">Top Projects Portofolio</h2>
|
||||
<p style="color:#64748b;font-size:1rem;max-width:42rem;margin:0;line-height:1.6;"
|
||||
data-i18n="grp_acaab_proj_desc">Jelajahi inovasi digital dan aplikasi nyata yang dikembangkan oleh mahasiswa Informatika UNTAN.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter panel -->
|
||||
<div id="acaab-proj-filters"
|
||||
style="background:rgba(255,255,255,0.8);backdrop-filter:blur(12px);padding:1rem;border-radius:0.5rem;box-shadow:0 1px 3px rgba(0,0,0,0.08);border-top:4px solid #feb401;margin-bottom:1.25rem;">
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<div
|
||||
style="display:flex;align-items:center;gap:0.5rem;font-size:0.875rem;font-weight:700;color:#003150;font-family:'Roboto Condensed',sans-serif;">
|
||||
<i class="fas fa-filter" style="font-size:0.8rem;"></i>
|
||||
<span>Filter Portofolio</span>
|
||||
</div>
|
||||
<button id="acaab-proj-reset"
|
||||
style="display:none;align-items:center;gap:0.25rem;font-size:0.75rem;font-weight:700;color:#64748b;background:none;border:none;cursor:pointer;"
|
||||
data-i18n="btn_reset_filter">
|
||||
<i class="fas fa-times" style="font-size:0.7rem;"></i> Reset Filter
|
||||
</button>
|
||||
</div>
|
||||
<div style="position:relative;margin-bottom:0.75rem;">
|
||||
<i class="fas fa-search"
|
||||
style="position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:#94a3b8;font-size:0.8rem;pointer-events:none;"></i>
|
||||
<input id="acaab-proj-search" type="search"
|
||||
style="width:100%;background:#f8fafc;color:#334155;font-size:0.875rem;border-radius:0.375rem;padding:0.5rem 0.75rem 0.5rem 2.25rem;border:1px solid #e2e8f0;outline:none;box-sizing:border-box;"
|
||||
placeholder="Cari karya..." data-i18n-placeholder="grp_acaab_proj_search">
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.625rem;">
|
||||
<select id="acaab-proj-cat" aria-label="Filter kategori portofolio"
|
||||
style="flex:1;min-width:130px;background:#f8fafc;color:#334155;font-size:0.8rem;font-weight:700;border-radius:0.375rem;padding:0.5rem 0.75rem;border:1px solid #e2e8f0;cursor:pointer;"></select>
|
||||
<select id="acaab-proj-year" aria-label="Filter tahun portofolio"
|
||||
style="flex:1;min-width:120px;background:#f8fafc;color:#334155;font-size:0.8rem;font-weight:700;border-radius:0.375rem;padding:0.5rem 0.75rem;border:1px solid #e2e8f0;cursor:pointer;"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll hint -->
|
||||
<p style="font-size:0.7rem;color:#94a3b8;text-align:center;margin-bottom:0.75rem;"
|
||||
data-i18n="grp_acaab_proj_scroll_hint">← Geser untuk melihat lebih banyak →</p>
|
||||
|
||||
<!-- Carousel track -->
|
||||
<div id="acaab-proj-track" class="acaab-carousel-track acaab-hide-scrollbar"
|
||||
style="display:flex;gap:1.25rem;overflow-x:auto;padding-bottom:1rem;"></div>
|
||||
|
||||
<!-- Scroll control buttons (bawah) -->
|
||||
<div style="display:flex;justify-content:center;align-items:center;gap:1rem;margin-top:1rem;">
|
||||
<button id="acaab-proj-prev" aria-label="Geser kiri"
|
||||
style="width:3rem;height:3rem;border-radius:50%;border:2px solid rgba(0,49,80,0.2);display:flex;align-items:center;justify-content:center;color:#003150;background:white;cursor:pointer;transition:all 0.2s;">
|
||||
<i class="fas fa-chevron-left" style="font-size:1rem;"></i>
|
||||
</button>
|
||||
<button id="acaab-proj-next" aria-label="Geser kanan"
|
||||
style="width:3rem;height:3rem;border-radius:50%;border:2px solid rgba(0,49,80,0.2);display:flex;align-items:center;justify-content:center;color:#003150;background:white;cursor:pointer;transition:all 0.2s;">
|
||||
<i class="fas fa-chevron-right" style="font-size:1rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Load more -->
|
||||
<div style="text-align:center;margin-top:1.5rem;">
|
||||
<button id="acaab-proj-more"
|
||||
style="display:none;padding:0.625rem 1.5rem;background:#003150;color:white;border:none;border-radius:0.5rem;font-weight:700;font-size:0.875rem;cursor:pointer;"
|
||||
data-i18n="grp_acaab_proj_load_more">Tampilkan Lebih Banyak</button>
|
||||
</div>
|
||||
</div><!-- /inner container -->
|
||||
</section>
|
||||
|
||||
<!-- ── CTA ────────────────────────────────────────────────────────────── -->
|
||||
<section class="not-prose acaab-full-bleed bg-untan-navy"
|
||||
style="padding:3.5rem 0; margin-bottom: 3rem;">
|
||||
<div class="container mx-auto px-4 lg:px-8">
|
||||
<!-- Inner card: kuning transparan dengan border, rounded-2xl -->
|
||||
<div
|
||||
style="background:rgba(254,180,1,0.08);border:1px solid rgba(254,180,1,0.2);border-radius:1rem;padding:2.5rem 2rem;">
|
||||
<div class="acaab-cta-inner">
|
||||
<!-- Kiri: teks -->
|
||||
<div>
|
||||
<span
|
||||
style="display:inline-block;background:#feb401;color:#0f172a;font-size:0.625rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;padding:0.2rem 0.5rem;border-radius:2px;margin-bottom:1rem;"
|
||||
data-i18n="grp_acaab_cta_badge">HUBUNGI KAMI</span>
|
||||
<h2 style="color:white;font-size:clamp(1.5rem,2.5vw,1.875rem);font-weight:700;margin:0 0 0.75rem;font-family:'Roboto Condensed',sans-serif;line-height:1.2;"
|
||||
data-i18n="grp_acaab_cta_title">Ingin Tahu Lebih Lanjut?</h2>
|
||||
<p style="color:#cbd5e1;font-size:0.9375rem;line-height:1.65;max-width:480px;margin:0;"
|
||||
data-i18n="grp_acaab_cta_desc">Tertarik bergabung dengan Program Studi
|
||||
Informatika UNTAN, mendaftarkan prestasi mahasiswa, atau menjalin kerja sama?
|
||||
Hubungi kami sekarang.</p>
|
||||
</div>
|
||||
<!-- Kanan: tombol WA -->
|
||||
<div style="flex-shrink:0;">
|
||||
<a id="acaab-cta-wa" href="#" target="_blank" rel="noopener noreferrer"
|
||||
style="display:inline-flex;align-items:center;gap:0.625rem;background:#25D366;color:white;padding:0.75rem 1.5rem;border-radius:9999px;font-weight:700;font-size:0.9375rem;text-decoration:none;box-shadow:0 4px 15px rgba(37,211,102,0.25);white-space:nowrap;transition:background 0.2s,transform 0.2s;">
|
||||
<i class="fab fa-whatsapp" style="font-size:1.125rem;"></i>
|
||||
<span data-i18n="grp_acaab_cta_btn">Chat via WhatsApp</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal container (diisi oleh main.js) -->
|
||||
<div id="acaab-modal" class="not-prose"></div>
|
||||
|
||||
<link rel="stylesheet" href="./assets/acaab.css">
|
||||
<div id="root" class="not-prose"></div>
|
||||
<script type="module" src="./assets/acaab.js"></script>
|
||||
<!-- ================= END: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1543,8 +1215,8 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full relative z-0 mt-4 pointer-events-none flex justify-center">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration"
|
||||
class="w-full h-auto object-cover object-bottom" style="object-position: bottom;">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration" class="w-full h-auto object-cover object-bottom"
|
||||
style="object-position: bottom;">
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1640,30 +1312,27 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn = document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function () {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function () {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn= document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function() {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function() {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
<script src="./localization.js"></script>
|
||||
<script src="./main.js"></script>
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* ACAAB localization extension.
|
||||
* Keep group-specific translations outside assets/global/localization.js
|
||||
* to avoid conflicts with the main project.
|
||||
*/
|
||||
|
||||
(function extendAcaabTranslations() {
|
||||
if (typeof translations === 'undefined') return;
|
||||
|
||||
const acaabTranslations = {
|
||||
id: {
|
||||
grp_acaab_hero_badge: "MAHASISWA & PRESTASI",
|
||||
grp_acaab_hero_title: "Sorotan dan Prestasi Karya Mahasiswa",
|
||||
grp_acaab_hero_desc: "Galeri pencapaian, penghargaan, dan portofolio inovasi teknologi yang dikembangkan oleh mahasiswa Program Studi Informatika Universitas Tanjungpura.",
|
||||
grp_acaab_stats_heading: "Pencapaian dalam Angka",
|
||||
grp_acaab_stats_total_ach: "Total Prestasi",
|
||||
grp_acaab_stats_total_proj: "Total Karya",
|
||||
grp_acaab_stats_intl: "Internasional",
|
||||
grp_acaab_stats_national: "Nasional",
|
||||
grp_acaab_stats_regional: "Regional",
|
||||
grp_acaab_stats_data_note: "Data dikumpulkan per",
|
||||
grp_acaab_ach_title: "Prestasi Mahasiswa",
|
||||
grp_acaab_ach_desc: "Daftar pencapaian dan penghargaan yang diraih oleh mahasiswa Informatika UNTAN di berbagai kompetisi.",
|
||||
grp_acaab_ach_search: "Cari prestasi...",
|
||||
grp_acaab_ach_filter_level: "Semua Level",
|
||||
grp_acaab_ach_filter_cat: "Semua Kategori",
|
||||
grp_acaab_ach_filter_year: "Semua Tahun",
|
||||
grp_acaab_ach_showing: "Menampilkan",
|
||||
grp_acaab_ach_results: "prestasi",
|
||||
grp_acaab_ach_load_more: "Tampilkan Lebih Banyak",
|
||||
grp_acaab_ach_show_less: "Tampilkan Lebih Sedikit",
|
||||
grp_acaab_ach_empty: "Tidak ada prestasi yang cocok dengan filter.",
|
||||
grp_acaab_ach_loading: "Memuat data prestasi...",
|
||||
grp_acaab_ach_error: "Gagal memuat data prestasi.",
|
||||
grp_acaab_proj_title: "Top Projects & Portofolio",
|
||||
grp_acaab_proj_desc: "Jelajahi inovasi digital dan aplikasi nyata yang dikembangkan oleh mahasiswa Informatika UNTAN.",
|
||||
grp_acaab_proj_scroll_hint: "Geser untuk melihat lebih banyak",
|
||||
grp_acaab_proj_search: "Cari karya...",
|
||||
grp_acaab_proj_filter_cat: "Semua Kategori",
|
||||
grp_acaab_proj_filter_year: "Semua Tahun",
|
||||
grp_acaab_proj_load_more: "Tampilkan Lebih Banyak",
|
||||
grp_acaab_proj_empty: "Tidak ada karya yang cocok dengan filter.",
|
||||
grp_acaab_proj_loading: "Memuat data karya...",
|
||||
grp_acaab_proj_error: "Gagal memuat data karya.",
|
||||
grp_acaab_modal_team: "Tim / Peserta",
|
||||
grp_acaab_modal_generation: "Angkatan",
|
||||
grp_acaab_modal_members: "Anggota Tim",
|
||||
grp_acaab_modal_visit: "Kunjungi Proyek",
|
||||
grp_acaab_modal_read_more: "Selengkapnya",
|
||||
grp_acaab_modal_no_link: "Link tidak tersedia",
|
||||
grp_acaab_stats_label_ach: "Prestasi Mahasiswa",
|
||||
grp_acaab_stats_label_proj: "Proyek Mahasiswa",
|
||||
grp_acaab_modal_year: "Tahun",
|
||||
grp_acaab_modal_competition: "Kompetisi",
|
||||
grp_acaab_modal_field: "Bidang",
|
||||
grp_acaab_modal_description: "Deskripsi",
|
||||
grp_acaab_modal_close: "Tutup",
|
||||
grp_acaab_modal_note: "Catatan Tambahan",
|
||||
grp_acaab_modal_batch_prefix: "Angkatan",
|
||||
grp_acaab_cta_badge: "HUBUNGI KAMI",
|
||||
grp_acaab_cta_title: "Ingin Tahu Lebih Lanjut?",
|
||||
grp_acaab_cta_desc: "Tertarik bergabung dengan Program Studi Informatika UNTAN, mendaftarkan prestasi mahasiswa, atau menjalin kerja sama? Hubungi kami sekarang.",
|
||||
grp_acaab_cta_btn: "Chat via WhatsApp",
|
||||
},
|
||||
en: {
|
||||
grp_acaab_hero_badge: "STUDENTS & ACHIEVEMENTS",
|
||||
grp_acaab_hero_title: "Student Spotlights & Achievements",
|
||||
grp_acaab_hero_desc: "Gallery of accomplishments, awards, and technology innovation portfolios developed by Informatics students of Tanjungpura University.",
|
||||
grp_acaab_stats_heading: "Achievements by the Numbers",
|
||||
grp_acaab_stats_total_ach: "Total Achievements",
|
||||
grp_acaab_stats_total_proj: "Total Projects",
|
||||
grp_acaab_stats_intl: "International",
|
||||
grp_acaab_stats_national: "National",
|
||||
grp_acaab_stats_regional: "Regional",
|
||||
grp_acaab_stats_data_note: "Data collected as of",
|
||||
grp_acaab_ach_title: "Student Achievements",
|
||||
grp_acaab_ach_desc: "List of awards and accomplishments by Informatics UNTAN students in various competitions.",
|
||||
grp_acaab_ach_search: "Search achievements...",
|
||||
grp_acaab_ach_filter_level: "All Levels",
|
||||
grp_acaab_ach_filter_cat: "All Categories",
|
||||
grp_acaab_ach_filter_year: "All Years",
|
||||
grp_acaab_ach_showing: "Showing",
|
||||
grp_acaab_ach_results: "achievements",
|
||||
grp_acaab_ach_load_more: "Show More",
|
||||
grp_acaab_ach_show_less: "Show Less",
|
||||
grp_acaab_ach_empty: "No achievements match the current filter.",
|
||||
grp_acaab_ach_loading: "Loading achievements...",
|
||||
grp_acaab_ach_error: "Failed to load achievements.",
|
||||
grp_acaab_proj_title: "Top Projects & Portfolios",
|
||||
grp_acaab_proj_desc: "Explore digital innovations and real-world applications developed by Informatics UNTAN students.",
|
||||
grp_acaab_proj_scroll_hint: "Scroll to see more",
|
||||
grp_acaab_proj_search: "Search projects...",
|
||||
grp_acaab_proj_filter_cat: "All Categories",
|
||||
grp_acaab_proj_filter_year: "All Years",
|
||||
grp_acaab_proj_load_more: "Show More",
|
||||
grp_acaab_proj_empty: "No projects match the current filter.",
|
||||
grp_acaab_proj_loading: "Loading projects...",
|
||||
grp_acaab_proj_error: "Failed to load projects.",
|
||||
grp_acaab_modal_team: "Team / Participant",
|
||||
grp_acaab_modal_generation: "Batch Year",
|
||||
grp_acaab_modal_members: "Team Members",
|
||||
grp_acaab_modal_visit: "Visit Project",
|
||||
grp_acaab_modal_read_more: "Read More",
|
||||
grp_acaab_modal_no_link: "Link not available",
|
||||
grp_acaab_stats_label_ach: "Student Achievements",
|
||||
grp_acaab_stats_label_proj: "Student Projects",
|
||||
grp_acaab_modal_year: "Year",
|
||||
grp_acaab_modal_competition: "Competition",
|
||||
grp_acaab_modal_field: "Field",
|
||||
grp_acaab_modal_description: "Description",
|
||||
grp_acaab_modal_close: "Close",
|
||||
grp_acaab_modal_note: "Additional Notes",
|
||||
grp_acaab_modal_batch_prefix: "Batch",
|
||||
grp_acaab_cta_badge: "CONTACT US",
|
||||
grp_acaab_cta_title: "Want to Know More?",
|
||||
grp_acaab_cta_desc: "Interested in joining the Informatics Study Program at UNTAN, registering a student achievement, or establishing a partnership? Contact us now.",
|
||||
grp_acaab_cta_btn: "Chat via WhatsApp",
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(translations.id, acaabTranslations.id);
|
||||
Object.assign(translations.en, acaabTranslations.en);
|
||||
}());
|
||||
@@ -1,837 +0,0 @@
|
||||
/**
|
||||
* main.js — ACAAB Prestasi & Karya Mahasiswa
|
||||
* Vanilla JS: fetch data Directus CMS, render sections, handle interactions.
|
||||
*/
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const API_BASE = 'https://api.ifuntanhub.dev';
|
||||
const LANG_KEY = 'if_untan_lang';
|
||||
|
||||
const BIDANG_MAP = {
|
||||
1: 'Kecerdasan Buatan', 2: 'Cyber Security', 3: 'Data',
|
||||
4: 'Software Engineering', 5: 'Competitive Programming', 6: 'IoT',
|
||||
7: 'Smart City', 8: 'Web3', 9: 'Karya Tulis Ilmiah', 10: 'Robotik',
|
||||
};
|
||||
|
||||
// ─── Utilities ────────────────────────────────────────────────────────────────
|
||||
|
||||
function escHtml(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getLang() {
|
||||
try {
|
||||
return localStorage.getItem(LANG_KEY) ||
|
||||
(navigator.language && navigator.language.startsWith('en') ? 'en' : 'id');
|
||||
} catch (_) { return 'id'; }
|
||||
}
|
||||
|
||||
// Baca dari translations global yang didefinisikan localization.js
|
||||
function t(key) {
|
||||
try {
|
||||
const lang = getLang();
|
||||
if (typeof translations !== 'undefined' && translations[lang] && translations[lang][key]) {
|
||||
return translations[lang][key];
|
||||
}
|
||||
} catch (_) {}
|
||||
return key;
|
||||
}
|
||||
|
||||
// ─── Data Transformation ──────────────────────────────────────────────────────
|
||||
|
||||
function extractRank(title) {
|
||||
const m = (title || '').match(/(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)/i);
|
||||
return m ? m[0] : 'Pemenang';
|
||||
}
|
||||
|
||||
function extractLevel(title, category) {
|
||||
const text = ((title || '') + ' ' + (category || '')).toLowerCase();
|
||||
if (/internasional|international|global|world|asean|unesco/.test(text)) return 'Internasional';
|
||||
if (/regional|kalbar|kalimantan|provinsi|kota|kabupaten|lokal/.test(text)) return 'Regional';
|
||||
return 'Nasional';
|
||||
}
|
||||
|
||||
function extractCompetition(title) {
|
||||
return (title || '')
|
||||
.replace(/^(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)\s+(di\s+|pada\s+|dalam\s+)?/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getImageUrl(item, seed) {
|
||||
if (item.galeri && Array.isArray(item.galeri) && item.galeri.length > 0) {
|
||||
const first = item.galeri[0];
|
||||
const imgId = typeof first === 'string' ? first : (first.directus_files_id || first.id);
|
||||
if (imgId) return `${API_BASE}/assets/${imgId}`;
|
||||
}
|
||||
if (item.foto) return `${API_BASE}/assets/${item.foto}`;
|
||||
return `https://picsum.photos/seed/${seed}/800/600`;
|
||||
}
|
||||
|
||||
function transformAchievement(item) {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_prestasi || 'Untitled Achievement';
|
||||
|
||||
let category = 'Umum';
|
||||
if (item.bidang_id) {
|
||||
category = (typeof item.bidang_id === 'object' && item.bidang_id.nama_bidang)
|
||||
? item.bidang_id.nama_bidang
|
||||
: (BIDANG_MAP[item.bidang_id] || 'Umum');
|
||||
}
|
||||
|
||||
const rank = extractRank(title);
|
||||
const level = extractLevel(title, category);
|
||||
const competition = extractCompetition(title);
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const anggota = Array.isArray(item.anggota) ? item.anggota : [];
|
||||
const team = item.nama_tim || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa');
|
||||
const avatar = (item.nama_tim || title || 'IF').substring(0, 2).toUpperCase();
|
||||
const description = item.deskripsi ||
|
||||
`${team} berhasil meraih prestasi luar biasa sebagai ${rank} dalam ajang ${competition} tahun ${year} pada bidang ${category.toLowerCase()}.`;
|
||||
|
||||
return {
|
||||
id, title, category, competition, rank, year, level, description,
|
||||
team,
|
||||
nim: item.nim || '',
|
||||
generation: item.angkatan ? `${t('grp_acaab_modal_batch_prefix')} ${item.angkatan}` : 'Informatika',
|
||||
image: getImageUrl(item, `prestasi-${id}`),
|
||||
avatar,
|
||||
members: anggota.map(m => ({ nama: m.nama || '', angkatan: m.angkatan ? parseInt(m.angkatan) : undefined })),
|
||||
};
|
||||
}
|
||||
|
||||
function transformProject(item) {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_proyek || 'Untitled Project';
|
||||
const anggota = Array.isArray(item.anggota) ? item.anggota : [];
|
||||
const studentName = item.nama_mahasiswa || (anggota.length > 0 ? anggota[0].nama : 'Mahasiswa');
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const description = item.deskripsi || 'Tidak ada deskripsi.';
|
||||
|
||||
let categories = [];
|
||||
if (item.bidang && Array.isArray(item.bidang)) {
|
||||
categories = item.bidang.map(b => {
|
||||
if (b.bidang_id && typeof b.bidang_id === 'object' && b.bidang_id.nama_bidang) return b.bidang_id.nama_bidang;
|
||||
if (typeof b.bidang_id === 'number') return BIDANG_MAP[b.bidang_id] || 'Umum';
|
||||
return 'Umum';
|
||||
}).filter(Boolean);
|
||||
}
|
||||
if (categories.length === 0) categories = ['Umum'];
|
||||
|
||||
let link = item.url || null;
|
||||
if (link && !/^https?:\/\//i.test(link)) link = `https://${link}`;
|
||||
|
||||
return {
|
||||
id, title, studentName, year, description, categories, link,
|
||||
nim: item.nim || '',
|
||||
image: getImageUrl(item, `project-${id}`),
|
||||
catatan: item.catatan || '',
|
||||
members: anggota.map(m => ({ nama: m.nama || '', nim: m.nim || '' })),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── API Fetch ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchAchievements() {
|
||||
const res = await fetch(`${API_BASE}/items/prestasi?fields=*.*`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const { data } = await res.json();
|
||||
return (data || [])
|
||||
.filter(item => item.status === 'published')
|
||||
.map(transformAchievement);
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
const res = await fetch(`${API_BASE}/items/portfolio?fields=*,bidang.bidang_id.nama_bidang,galeri.*`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const { data } = await res.json();
|
||||
return (data || [])
|
||||
.filter(item => item.status === 'published')
|
||||
.map(transformProject);
|
||||
}
|
||||
|
||||
// ─── App State ────────────────────────────────────────────────────────────────
|
||||
|
||||
let achievements = [];
|
||||
let projects = [];
|
||||
let achVisible = 3;
|
||||
let projVisible = 6;
|
||||
|
||||
let achFilter = { search: '', level: '', cat: '', year: '' };
|
||||
let projFilter = { search: '', cat: '', year: '' };
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
initWaButton();
|
||||
|
||||
// Loading placeholders
|
||||
showSkeleton('acaab-ach-grid', 3);
|
||||
showSkeleton('acaab-proj-track', 4);
|
||||
|
||||
const [achResult, projResult] = await Promise.allSettled([
|
||||
fetchAchievements(),
|
||||
fetchProjects(),
|
||||
]);
|
||||
|
||||
if (achResult.status === 'fulfilled') {
|
||||
achievements = achResult.value.sort((a, b) => b.year - a.year);
|
||||
} else {
|
||||
console.error('[ACAAB] achievements error:', achResult.reason);
|
||||
showError('acaab-ach-grid', t('grp_acaab_ach_error'));
|
||||
}
|
||||
|
||||
if (projResult.status === 'fulfilled') {
|
||||
projects = projResult.value;
|
||||
} else {
|
||||
console.error('[ACAAB] projects error:', projResult.reason);
|
||||
showError('acaab-proj-track', t('grp_acaab_proj_error'));
|
||||
}
|
||||
|
||||
initStats();
|
||||
initAchievements();
|
||||
initProjects();
|
||||
updateDataNote();
|
||||
});
|
||||
|
||||
// Re-render saat bahasa berubah dari kit navbar
|
||||
document.addEventListener('languageChanged', () => {
|
||||
updateStats();
|
||||
renderAchievements();
|
||||
buildAchFilterOptions();
|
||||
renderProjects();
|
||||
buildProjFilterOptions();
|
||||
updateWaButton();
|
||||
updateDataNote();
|
||||
updateSearchPlaceholders();
|
||||
});
|
||||
|
||||
// ─── Skeleton / Error ─────────────────────────────────────────────────────────
|
||||
|
||||
function showSkeleton(containerId, count) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
el.innerHTML = Array(count).fill(0).map(() =>
|
||||
`<div class="acaab-skeleton" style="height:220px;border-radius:0.75rem;"></div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function showError(containerId, msg) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (el) el.innerHTML = `<p style="color:#ef4444;padding:1rem 0;grid-column:1/-1;">${escHtml(msg)}</p>`;
|
||||
}
|
||||
|
||||
// ─── Stats ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function initStats() {
|
||||
updateStats();
|
||||
|
||||
const statsEl = document.getElementById('acaab-stats');
|
||||
if (!statsEl) return;
|
||||
|
||||
const intl = achievements.filter(a => a.level === 'Internasional').length;
|
||||
const national = achievements.filter(a => a.level === 'Nasional').length;
|
||||
const regional = achievements.filter(a => a.level === 'Regional').length;
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
if (!entry.isIntersecting) return;
|
||||
animateCount('stat-total-ach', achievements.length, 1600);
|
||||
animateCount('stat-total-proj', projects.length, 1600);
|
||||
animateCount('stat-intl', intl, 1200);
|
||||
animateCount('stat-national', national, 1350);
|
||||
animateCount('stat-regional', regional, 1500);
|
||||
observer.disconnect();
|
||||
});
|
||||
}, { threshold: 0.4 });
|
||||
|
||||
observer.observe(statsEl);
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
|
||||
set('stat-total-ach', achievements.length);
|
||||
set('stat-total-proj', projects.length);
|
||||
set('stat-intl', achievements.filter(a => a.level === 'Internasional').length);
|
||||
set('stat-national', achievements.filter(a => a.level === 'Nasional').length);
|
||||
set('stat-regional', achievements.filter(a => a.level === 'Regional').length);
|
||||
}
|
||||
|
||||
function animateCount(elId, target, duration) {
|
||||
const el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
const start = performance.now();
|
||||
(function step(now) {
|
||||
const p = Math.min((now - start) / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - p, 4); // easeOutQuart
|
||||
el.textContent = Math.round(target * eased).toLocaleString();
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
})(start);
|
||||
}
|
||||
|
||||
function updateDataNote() {
|
||||
const el = document.getElementById('acaab-data-note');
|
||||
if (!el) return;
|
||||
const lang = getLang();
|
||||
const locale = lang === 'en' ? 'en-US' : 'id-ID';
|
||||
const date = new Date().toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
el.textContent = `${t('grp_acaab_stats_data_note')} ${date}`;
|
||||
}
|
||||
|
||||
// ─── Achievements ─────────────────────────────────────────────────────────────
|
||||
|
||||
const ACH_INIT = 3;
|
||||
const ACH_MORE = 3;
|
||||
|
||||
const LEVEL_CLASS = {
|
||||
Internasional: 'acaab-badge-intl',
|
||||
Nasional: 'acaab-badge-nasional',
|
||||
Regional: 'acaab-badge-regional',
|
||||
};
|
||||
|
||||
function initAchievements() {
|
||||
buildAchFilterOptions();
|
||||
renderAchievements();
|
||||
bindAchEvents();
|
||||
}
|
||||
|
||||
function buildAchFilterOptions() {
|
||||
const levelSel = document.getElementById('acaab-ach-level');
|
||||
const catSel = document.getElementById('acaab-ach-cat');
|
||||
const yearSel = document.getElementById('acaab-ach-year');
|
||||
if (!levelSel || !catSel || !yearSel) return;
|
||||
|
||||
const levels = ['Internasional', 'Nasional', 'Regional'];
|
||||
const cats = [...new Set(achievements.map(a => a.category))].sort();
|
||||
const years = [...new Set(achievements.map(a => String(a.year)))].filter(Boolean).sort((a, b) => b - a);
|
||||
|
||||
levelSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_ach_filter_level'))}</option>` +
|
||||
levels.map(l => `<option value="${escHtml(l)}"${achFilter.level === l ? ' selected' : ''}>${escHtml(l)}</option>`).join('');
|
||||
catSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_ach_filter_cat'))}</option>` +
|
||||
cats.map(c => `<option value="${escHtml(c)}"${achFilter.cat === c ? ' selected' : ''}>${escHtml(c)}</option>`).join('');
|
||||
yearSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_ach_filter_year'))}</option>` +
|
||||
years.map(y => `<option value="${escHtml(y)}"${achFilter.year === y ? ' selected' : ''}>${escHtml(y)}</option>`).join('');
|
||||
}
|
||||
|
||||
function filterAchievements() {
|
||||
const q = achFilter.search.toLowerCase();
|
||||
return achievements.filter(a => {
|
||||
const matchSearch = !q || [a.title, a.team, a.category, a.competition]
|
||||
.some(s => (s || '').toLowerCase().includes(q));
|
||||
const matchLevel = !achFilter.level || a.level === achFilter.level;
|
||||
const matchCat = !achFilter.cat || a.category === achFilter.cat;
|
||||
const matchYear = !achFilter.year || String(a.year) === achFilter.year;
|
||||
return matchSearch && matchLevel && matchCat && matchYear;
|
||||
});
|
||||
}
|
||||
|
||||
function renderAchievements() {
|
||||
const grid = document.getElementById('acaab-ach-grid');
|
||||
const countEl = document.getElementById('acaab-ach-count');
|
||||
const moreBtn = document.getElementById('acaab-ach-more');
|
||||
const lessBtn = document.getElementById('acaab-ach-less');
|
||||
const resetBtn= document.getElementById('acaab-ach-reset');
|
||||
if (!grid) return;
|
||||
|
||||
const filtered = filterAchievements();
|
||||
const visible = filtered.slice(0, achVisible);
|
||||
const hasFilter = achFilter.search || achFilter.level || achFilter.cat || achFilter.year;
|
||||
|
||||
if (countEl) countEl.textContent =
|
||||
`${t('grp_acaab_ach_showing')} ${filtered.length} ${t('grp_acaab_ach_results')}`;
|
||||
if (resetBtn) resetBtn.style.display = hasFilter ? 'flex' : 'none';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = `<p style="color:#64748b;padding:0.5rem 0;grid-column:1/-1;">${escHtml(t('grp_acaab_ach_empty'))}</p>`;
|
||||
if (moreBtn) moreBtn.style.display = 'none';
|
||||
if (lessBtn) lessBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = visible.map((a, i) => achCardHtml(a, i)).join('');
|
||||
|
||||
if (moreBtn) moreBtn.style.display = achVisible < filtered.length ? 'inline-block' : 'none';
|
||||
if (lessBtn) lessBtn.style.display = achVisible > ACH_INIT ? 'inline-block' : 'none';
|
||||
|
||||
grid.querySelectorAll('.acaab-ach-card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
// Biarkan tombol yang merupakan link tetap berfungsi
|
||||
if (e.target.closest('a')) return;
|
||||
const ach = achievements.find(a => a.id === card.dataset.id);
|
||||
if (ach) openAchModal(ach);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function achCardHtml(a, idx) {
|
||||
return `
|
||||
<article class="acaab-ach-card acaab-fade-in" data-id="${escHtml(a.id)}" style="animation-delay:${idx * 0.05}s;cursor:pointer;">
|
||||
<div class="acaab-ach-top-bar"></div>
|
||||
<!-- Image h-56 (224px) -->
|
||||
<div style="position:relative;height:224px;overflow:hidden;background:#f1f5f9;">
|
||||
<img src="${escHtml(a.image)}" alt="${escHtml(a.title)}" loading="lazy" class="acaab-card-img"
|
||||
style="width:100%;height:100%;object-fit:cover;"
|
||||
onerror="this.src='https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN'">
|
||||
<!-- Rank badge top-left: white pill dengan trophy icon -->
|
||||
<div style="position:absolute;top:0.75rem;left:0.75rem;background:rgba(255,255,255,0.95);backdrop-filter:blur(4px);color:#003150;font-weight:700;padding:0.25rem 0.625rem;border-radius:9999px;box-shadow:0 1px 3px rgba(0,0,0,0.15);display:flex;align-items:center;gap:0.375rem;font-size:0.72rem;z-index:2;">
|
||||
<i class="fas fa-trophy" style="color:#feb401;font-size:0.65rem;"></i>
|
||||
${escHtml(a.rank)}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content p-6 -->
|
||||
<div style="padding:1.5rem;display:flex;flex-direction:column;flex-grow:1;">
|
||||
<!-- Category + Year row -->
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.75rem;">
|
||||
<span style="color:#1e40af;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;">${escHtml(a.category)}</span>
|
||||
<span style="color:#94a3b8;font-size:0.72rem;font-weight:500;display:flex;align-items:center;gap:0.25rem;border-left:1px solid #e2e8f0;padding-left:0.625rem;">
|
||||
<i class="fas fa-calendar-alt" style="font-size:0.6rem;"></i> ${escHtml(String(a.year))}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<h3 class="acaab-card-title acaab-line-clamp-2" style="font-size:1.125rem;font-weight:700;color:#003150;margin:0 0 0.75rem;line-height:1.35;font-family:'Roboto Condensed',sans-serif;">${escHtml(a.title)}</h3>
|
||||
<!-- Description 3 lines -->
|
||||
<p class="acaab-line-clamp-3" style="color:#475569;font-size:0.875rem;line-height:1.6;margin:0 0 1.5rem;flex-grow:1;">${escHtml(a.description)}</p>
|
||||
<!-- Bottom -->
|
||||
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-top:auto;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||
<!-- Avatar + team + generation -->
|
||||
<div style="display:flex;align-items:center;gap:0.625rem;">
|
||||
<div style="width:2rem;height:2rem;border-radius:0.375rem;background:rgba(0,49,80,0.05);border:1px solid rgba(0,49,80,0.1);display:flex;align-items:center;justify-content:center;color:#003150;font-weight:700;font-size:0.65rem;flex-shrink:0;">${escHtml(a.avatar)}</div>
|
||||
<div>
|
||||
<span style="font-weight:700;color:#1e293b;display:block;font-size:0.8125rem;line-height:1.2;">${escHtml(a.team)}</span>
|
||||
<span style="color:#64748b;display:block;font-size:0.72rem;margin-top:0.1rem;">${escHtml(a.generation)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Level badge -->
|
||||
<span style="background:#f1f5f9;color:#475569;font-size:0.575rem;padding:0.2rem 0.5rem;border-radius:0.25rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;white-space:nowrap;">${escHtml(a.level)}</span>
|
||||
</div>
|
||||
<!-- Full-width CTA button -->
|
||||
<button class="acaab-ach-readmore acaab-ach-read-btn" data-id="${escHtml(a.id)}"
|
||||
style="width:100%;padding:0.625rem;border-radius:0.375rem;background:rgba(0,49,80,0.05);color:#003150;font-weight:700;font-size:0.875rem;border:none;cursor:pointer;transition:background 0.2s,color 0.2s;min-height:44px;">
|
||||
${escHtml(t('grp_acaab_modal_read_more'))}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
function bindAchEvents() {
|
||||
const search = document.getElementById('acaab-ach-search');
|
||||
const level = document.getElementById('acaab-ach-level');
|
||||
const cat = document.getElementById('acaab-ach-cat');
|
||||
const year = document.getElementById('acaab-ach-year');
|
||||
const reset = document.getElementById('acaab-ach-reset');
|
||||
const moreBtn = document.getElementById('acaab-ach-more');
|
||||
const lessBtn = document.getElementById('acaab-ach-less');
|
||||
|
||||
const onChange = () => {
|
||||
achFilter.search = search ? search.value.trim() : '';
|
||||
achFilter.level = level ? level.value : '';
|
||||
achFilter.cat = cat ? cat.value : '';
|
||||
achFilter.year = year ? year.value : '';
|
||||
achVisible = ACH_INIT;
|
||||
renderAchievements();
|
||||
};
|
||||
|
||||
if (search) search.addEventListener('input', onChange);
|
||||
if (level) level.addEventListener('change', onChange);
|
||||
if (cat) cat.addEventListener('change', onChange);
|
||||
if (year) year.addEventListener('change', onChange);
|
||||
|
||||
if (reset) reset.addEventListener('click', () => {
|
||||
achFilter = { search: '', level: '', cat: '', year: '' };
|
||||
if (search) search.value = '';
|
||||
if (level) level.value = '';
|
||||
if (cat) cat.value = '';
|
||||
if (year) year.value = '';
|
||||
achVisible = ACH_INIT;
|
||||
renderAchievements();
|
||||
});
|
||||
|
||||
if (moreBtn) moreBtn.addEventListener('click', () => { achVisible += ACH_MORE; renderAchievements(); });
|
||||
if (lessBtn) lessBtn.addEventListener('click', () => {
|
||||
achVisible = ACH_INIT;
|
||||
document.getElementById('acaab-ach-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
renderAchievements();
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Achievement Modal ────────────────────────────────────────────────────────
|
||||
|
||||
function infoRow(icon, label, value) {
|
||||
if (!value) return '';
|
||||
return `<div style="display:flex;gap:0.625rem;align-items:flex-start;">
|
||||
<span style="color:#feb401;margin-top:0.125rem;flex-shrink:0;"><i class="fas fa-${icon}" style="font-size:0.75rem;"></i></span>
|
||||
<div>
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.125rem;line-height:1;">${escHtml(label)}</p>
|
||||
<p style="font-size:0.875rem;font-weight:600;color:#1e293b;margin:0;line-height:1.3;">${escHtml(value)}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function memberItem(nama, sub) {
|
||||
const initial = (nama || '?').charAt(0).toUpperCase();
|
||||
return `<li style="display:flex;align-items:center;gap:0.5rem;background:#f8fafc;padding:0.5rem 0.75rem;border-radius:0.375rem;">
|
||||
<span style="width:1.5rem;height:1.5rem;border-radius:50%;background:rgba(254,180,1,0.2);color:#b45309;display:flex;align-items:center;justify-content:center;font-size:0.72rem;font-weight:700;flex-shrink:0;">${initial}</span>
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
<span style="font-size:0.875rem;font-weight:600;color:#334155;line-height:1.2;">${escHtml(nama)}</span>
|
||||
${sub ? `<span style="font-size:0.65rem;color:#94a3b8;margin-top:0.125rem;">${escHtml(sub)}</span>` : ''}
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
function openAchModal(a) {
|
||||
const membersHtml = a.members && a.members.length > 0 ? `
|
||||
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1rem;">
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_members')}</p>
|
||||
<ul style="list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
|
||||
${a.members.map(m => memberItem(m.nama, m.angkatan ? `${t('grp_acaab_modal_batch_prefix')} ${m.angkatan}` : '')).join('')}
|
||||
</ul>
|
||||
</div>` : '';
|
||||
|
||||
renderModal(`
|
||||
<!-- Image header -->
|
||||
<div style="position:relative;border-radius:0.75rem 0.75rem 0 0;background:#0f172a;flex-shrink:0;">
|
||||
<img src="${escHtml(a.image)}" alt="${escHtml(a.title)}" style="width:100%;max-height:450px;object-fit:contain;display:block;"
|
||||
onerror="this.src='https://placehold.co/800x600/0f172a/FDB813?text=Informatika+UNTAN'">
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.1) 40%,transparent 100%);pointer-events:none;"></div>
|
||||
<!-- Close button: white rounded-full -->
|
||||
<button id="acaab-modal-close" aria-label="Tutup"
|
||||
style="position:absolute;top:0.75rem;right:0.75rem;width:2.25rem;height:2.25rem;background:rgba(255,255,255,0.9);backdrop-filter:blur(4px);border:none;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">
|
||||
<i class="fas fa-times" style="color:#334155;font-size:0.875rem;"></i>
|
||||
</button>
|
||||
<!-- Two badges bottom-left -->
|
||||
<div style="position:absolute;bottom:1rem;left:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||
<span style="background:#feb401;color:#0f172a;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;display:flex;align-items:center;gap:0.375rem;">
|
||||
<i class="fas fa-trophy" style="font-size:0.65rem;"></i>${escHtml(a.rank)}
|
||||
</span>
|
||||
<span style="background:rgba(255,255,255,0.2);backdrop-filter:blur(4px);color:white;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;text-transform:uppercase;letter-spacing:0.05em;">
|
||||
${escHtml(a.level)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div style="padding:1.5rem;">
|
||||
<span style="color:#1e40af;font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;">${escHtml(a.category)}</span>
|
||||
<h2 style="font-size:1.125rem;font-weight:700;color:#003150;line-height:1.3;margin:0.25rem 0 1.25rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(a.title)}</h2>
|
||||
<!-- Info grid: slate-50 bg -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;padding:1rem;background:#f8fafc;border-radius:0.5rem;margin-bottom:1.25rem;">
|
||||
${infoRow('users', t('grp_acaab_modal_team'), a.team)}
|
||||
${a.nim ? infoRow('id-card', 'NIM', a.nim) : ''}
|
||||
${infoRow('user-graduate', t('grp_acaab_modal_generation'), a.generation)}
|
||||
${infoRow('calendar-alt', t('grp_acaab_modal_year'), String(a.year))}
|
||||
${infoRow('award', t('grp_acaab_modal_competition'), a.competition)}
|
||||
${infoRow('tag', t('grp_acaab_modal_field'), a.category)}
|
||||
</div>
|
||||
${membersHtml}
|
||||
<!-- Description -->
|
||||
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;">
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_description')}</p>
|
||||
<p style="color:#475569;font-size:0.875rem;line-height:1.65;margin:0;">${escHtml(a.description)}</p>
|
||||
</div>
|
||||
<!-- Close button full-width -->
|
||||
<button id="acaab-modal-close-btn" style="margin-top:1.5rem;width:100%;padding:0.75rem;background:#003150;color:white;font-weight:700;border:none;border-radius:0.5rem;cursor:pointer;font-size:0.875rem;">
|
||||
${t('grp_acaab_modal_close')}
|
||||
</button>
|
||||
</div>
|
||||
`, true);
|
||||
}
|
||||
|
||||
// ─── Projects ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const PROJ_INIT = 6;
|
||||
const PROJ_MORE = 3;
|
||||
|
||||
function initProjects() {
|
||||
buildProjFilterOptions();
|
||||
renderProjects();
|
||||
bindProjEvents();
|
||||
}
|
||||
|
||||
function buildProjFilterOptions() {
|
||||
const catSel = document.getElementById('acaab-proj-cat');
|
||||
const yearSel = document.getElementById('acaab-proj-year');
|
||||
if (!catSel || !yearSel) return;
|
||||
|
||||
const cats = [...new Set(projects.flatMap(p => p.categories))].sort();
|
||||
const years = [...new Set(projects.map(p => String(p.year)))].filter(Boolean).sort((a, b) => b - a);
|
||||
|
||||
catSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_proj_filter_cat'))}</option>` +
|
||||
cats.map(c => `<option value="${escHtml(c)}"${projFilter.cat === c ? ' selected' : ''}>${escHtml(c)}</option>`).join('');
|
||||
yearSel.innerHTML = `<option value="">${escHtml(t('grp_acaab_proj_filter_year'))}</option>` +
|
||||
years.map(y => `<option value="${escHtml(y)}"${projFilter.year === y ? ' selected' : ''}>${escHtml(y)}</option>`).join('');
|
||||
}
|
||||
|
||||
function filterProjects() {
|
||||
const q = projFilter.search.toLowerCase();
|
||||
const result = projects.filter(p => {
|
||||
const matchSearch = !q || [p.title, p.studentName, p.description, ...p.categories]
|
||||
.some(s => (s || '').toLowerCase().includes(q));
|
||||
const matchCat = !projFilter.cat || p.categories.includes(projFilter.cat);
|
||||
const matchYear = !projFilter.year || String(p.year) === projFilter.year;
|
||||
return matchSearch && matchCat && matchYear;
|
||||
});
|
||||
|
||||
// Sort descending by year
|
||||
result.sort((a, b) => (b.year || 0) - (a.year || 0));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderProjects() {
|
||||
const track = document.getElementById('acaab-proj-track');
|
||||
const moreBtn = document.getElementById('acaab-proj-more');
|
||||
const resetBtn = document.getElementById('acaab-proj-reset');
|
||||
if (!track) return;
|
||||
|
||||
const filtered = filterProjects();
|
||||
const visible = filtered.slice(0, projVisible);
|
||||
const hasFilter = projFilter.search || projFilter.cat || projFilter.year;
|
||||
|
||||
if (resetBtn) resetBtn.style.display = hasFilter ? 'flex' : 'none';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
track.innerHTML = `<p style="color:#64748b;padding:0.5rem 0;">${escHtml(t('grp_acaab_proj_empty'))}</p>`;
|
||||
if (moreBtn) moreBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
track.innerHTML = visible.map((p, i) => projCardHtml(p, i)).join('');
|
||||
if (moreBtn) moreBtn.style.display = projVisible < filtered.length ? 'inline-block' : 'none';
|
||||
|
||||
// Responsif: Sembunyikan scroll button & center jika cuma 1 data
|
||||
const prevBtn = document.getElementById('acaab-proj-prev');
|
||||
const nextBtn = document.getElementById('acaab-proj-next');
|
||||
if (prevBtn) prevBtn.style.display = filtered.length <= 1 ? 'none' : 'flex';
|
||||
if (nextBtn) nextBtn.style.display = filtered.length <= 1 ? 'none' : 'flex';
|
||||
track.style.justifyContent = filtered.length === 1 ? 'center' : 'flex-start';
|
||||
|
||||
track.querySelectorAll('.acaab-proj-card-wrap').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('a') && e.target.closest('a').href) return;
|
||||
const proj = projects.find(p => p.id === card.dataset.id);
|
||||
if (proj) openProjModal(proj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function projCardHtml(p, idx) {
|
||||
const catBadges = p.categories.map(c => `
|
||||
<span style="display:inline-flex;align-items:center;gap:0.2rem;font-size:0.6rem;font-weight:700;color:#feb401;text-transform:uppercase;letter-spacing:0.05em;background:rgba(0,0,0,0.3);padding:0.2rem 0.45rem;border-radius:0.2rem;backdrop-filter:blur(4px);">
|
||||
<i class="fas fa-tag" style="font-size:0.5rem;"></i>${escHtml(c)}
|
||||
</span>`).join('');
|
||||
|
||||
return `
|
||||
<div class="acaab-proj-card acaab-proj-card-wrap acaab-fade-in" data-id="${escHtml(p.id)}" style="animation-delay:${idx * 0.05}s;cursor:pointer;">
|
||||
<div class="acaab-proj-left-bar"></div>
|
||||
<!-- Image h-52 (208px) dengan title overlay -->
|
||||
<div style="height:208px;overflow:hidden;position:relative;">
|
||||
<img src="${escHtml(p.image)}" alt="${escHtml(p.title)}" loading="lazy" class="acaab-card-img"
|
||||
style="width:100%;height:100%;object-fit:cover;"
|
||||
onerror="this.src='https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN'">
|
||||
<!-- Gradient overlay: title + categories di dalam gambar -->
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,49,80,0.85),transparent);padding:1.25rem;display:flex;flex-direction:column;justify-content:flex-end;pointer-events:none;">
|
||||
<h4 style="font-size:1rem;font-weight:700;color:white;line-height:1.25;margin:0 0 0.375rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(p.title)}</h4>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.3rem;">${catBadges}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Body p-5 -->
|
||||
<div style="padding:1.25rem;display:flex;flex-direction:column;flex-grow:1;">
|
||||
<!-- Meta: student + year -->
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.625rem;margin-bottom:0.75rem;font-size:0.72rem;color:#64748b;font-weight:500;">
|
||||
${p.studentName ? `<span style="display:flex;align-items:center;gap:0.25rem;"><i class="fas fa-user" style="color:#94a3b8;font-size:0.6rem;"></i>${escHtml(p.studentName)}</span>` : ''}
|
||||
${p.year ? `<span style="display:flex;align-items:center;gap:0.25rem;"><i class="fas fa-calendar-alt" style="color:#94a3b8;font-size:0.6rem;"></i>${escHtml(String(p.year))}</span>` : ''}
|
||||
</div>
|
||||
<!-- Description 3 lines -->
|
||||
<p class="acaab-line-clamp-3" style="color:#475569;font-size:0.875rem;line-height:1.6;margin:0 0 1.25rem;flex-grow:1;">${escHtml(p.description)}</p>
|
||||
<!-- Actions -->
|
||||
<div style="display:flex;align-items:center;gap:0.625rem;margin-top:auto;">
|
||||
<button class="acaab-proj-readmore" data-id="${escHtml(p.id)}"
|
||||
style="color:#003150;font-weight:700;font-size:0.875rem;background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:0.375rem;min-height:44px;">
|
||||
${escHtml(t('grp_acaab_modal_read_more'))} <i class="fas fa-arrow-right" style="font-size:0.7rem;"></i>
|
||||
</button>
|
||||
${p.link ? `
|
||||
<a href="${escHtml(p.link)}" target="_blank" rel="noopener noreferrer"
|
||||
style="margin-left:auto;display:flex;align-items:center;gap:0.375rem;font-size:0.72rem;font-weight:700;color:#64748b;border:1px solid #e2e8f0;border-radius:0.375rem;padding:0.375rem 0.625rem;text-decoration:none;min-height:44px;transition:color 0.2s,border-color 0.2s;">
|
||||
<i class="fas fa-external-link-alt" style="font-size:0.65rem;"></i>${escHtml(t('grp_acaab_modal_visit'))}
|
||||
</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function bindProjEvents() {
|
||||
const search = document.getElementById('acaab-proj-search');
|
||||
const cat = document.getElementById('acaab-proj-cat');
|
||||
const year = document.getElementById('acaab-proj-year');
|
||||
const reset = document.getElementById('acaab-proj-reset');
|
||||
const moreBtn = document.getElementById('acaab-proj-more');
|
||||
const prevBtn = document.getElementById('acaab-proj-prev');
|
||||
const nextBtn = document.getElementById('acaab-proj-next');
|
||||
|
||||
const onChange = () => {
|
||||
projFilter.search = search ? search.value.trim() : '';
|
||||
projFilter.cat = cat ? cat.value : '';
|
||||
projFilter.year = year ? year.value : '';
|
||||
projVisible = PROJ_INIT;
|
||||
renderProjects();
|
||||
};
|
||||
|
||||
if (search) search.addEventListener('input', onChange);
|
||||
if (cat) cat.addEventListener('change', onChange);
|
||||
if (year) year.addEventListener('change', onChange);
|
||||
|
||||
if (reset) reset.addEventListener('click', () => {
|
||||
projFilter = { search: '', cat: '', year: '' };
|
||||
if (search) search.value = '';
|
||||
if (cat) cat.value = '';
|
||||
if (year) year.value = '';
|
||||
projVisible = PROJ_INIT;
|
||||
renderProjects();
|
||||
});
|
||||
|
||||
if (moreBtn) moreBtn.addEventListener('click', () => { projVisible += PROJ_MORE; renderProjects(); });
|
||||
|
||||
const track = document.getElementById('acaab-proj-track');
|
||||
if (prevBtn && track) prevBtn.addEventListener('click', () => {
|
||||
const cardW = (track.firstElementChild ? track.firstElementChild.offsetWidth : 300) + 20;
|
||||
track.scrollBy({ left: -cardW, behavior: 'smooth' });
|
||||
});
|
||||
if (nextBtn && track) nextBtn.addEventListener('click', () => {
|
||||
const cardW = (track.firstElementChild ? track.firstElementChild.offsetWidth : 300) + 20;
|
||||
track.scrollBy({ left: cardW, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Project Modal ────────────────────────────────────────────────────────────
|
||||
|
||||
function openProjModal(p) {
|
||||
const membersHtml = p.members && p.members.length > 0 ? `
|
||||
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1rem;">
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_members')}</p>
|
||||
<ul style="list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;">
|
||||
${p.members.map(m => memberItem(m.nama, m.nim || '')).join('')}
|
||||
</ul>
|
||||
</div>` : '';
|
||||
|
||||
const catBadgesImg = p.categories.map(c => `
|
||||
<span style="background:#feb401;color:#0f172a;font-size:0.72rem;font-weight:700;padding:0.3rem 0.75rem;border-radius:0.25rem;display:inline-flex;align-items:center;gap:0.375rem;text-transform:uppercase;letter-spacing:0.05em;">
|
||||
<i class="fas fa-tag" style="font-size:0.6rem;"></i>${escHtml(c)}
|
||||
</span>`).join('');
|
||||
|
||||
const linkSection = p.link
|
||||
? `<a href="${escHtml(p.link)}" target="_blank" rel="noopener noreferrer"
|
||||
style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;padding:0.75rem;background:#003150;color:white;font-weight:700;border-radius:0.5rem;font-size:0.875rem;text-decoration:none;margin-bottom:0.625rem;">
|
||||
<i class="fas fa-external-link-alt" style="font-size:0.8rem;"></i>${escHtml(t('grp_acaab_modal_visit'))}
|
||||
</a>`
|
||||
: `<div style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;padding:0.75rem;background:#f1f5f9;color:#94a3b8;font-weight:700;border-radius:0.5rem;font-size:0.875rem;margin-bottom:0.625rem;cursor:not-allowed;">
|
||||
<i class="fas fa-file-alt" style="font-size:0.8rem;"></i>${escHtml(t('grp_acaab_modal_no_link'))}
|
||||
</div>`;
|
||||
|
||||
renderModal(`
|
||||
<!-- Image header -->
|
||||
<div style="position:relative;border-radius:0.75rem 0.75rem 0 0;background:#0f172a;flex-shrink:0;">
|
||||
<img src="${escHtml(p.image)}" alt="${escHtml(p.title)}" style="width:100%;max-height:450px;object-fit:contain;display:block;"
|
||||
onerror="this.src='https://placehold.co/800x600/0f172a/FDB813?text=Informatika+UNTAN'">
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,0.8) 0%,rgba(0,0,0,0.1) 40%,transparent 100%);pointer-events:none;"></div>
|
||||
<!-- Close button -->
|
||||
<button id="acaab-modal-close" aria-label="Tutup"
|
||||
style="position:absolute;top:0.75rem;right:0.75rem;width:2.25rem;height:2.25rem;background:rgba(255,255,255,0.9);backdrop-filter:blur(4px);border:none;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,0.2);">
|
||||
<i class="fas fa-times" style="color:#334155;font-size:0.875rem;"></i>
|
||||
</button>
|
||||
<!-- Category badges bottom-left -->
|
||||
<div style="position:absolute;bottom:1rem;left:1rem;display:flex;flex-wrap:wrap;gap:0.375rem;">${catBadgesImg}</div>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div style="padding:1.5rem;">
|
||||
<h2 style="font-size:1.125rem;font-weight:700;color:#003150;line-height:1.3;margin:0 0 1.25rem;font-family:'Roboto Condensed',sans-serif;">${escHtml(p.title)}</h2>
|
||||
<!-- Info grid -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;padding:1rem;background:#f8fafc;border-radius:0.5rem;margin-bottom:1.25rem;">
|
||||
${p.studentName ? infoRow('users', t('grp_acaab_modal_team'), p.studentName) : ''}
|
||||
${p.nim ? infoRow('id-card', 'NIM', p.nim) : ''}
|
||||
${infoRow('calendar-alt', t('grp_acaab_modal_year'), String(p.year))}
|
||||
${infoRow('tag', t('grp_acaab_modal_field'), p.categories.join(', '))}
|
||||
</div>
|
||||
${membersHtml}
|
||||
<!-- Description -->
|
||||
<div style="border-top:1px solid #f1f5f9;padding-top:1rem;margin-bottom:1.25rem;">
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#94a3b8;margin:0 0 0.5rem;">${t('grp_acaab_modal_description')}</p>
|
||||
<p style="color:#475569;font-size:0.875rem;line-height:1.65;margin:0;">${escHtml(p.description)}</p>
|
||||
${p.catatan ? `
|
||||
<div style="margin-top:1rem;padding:0.75rem;background:rgba(239,246,255,0.5);border:1px solid #bfdbfe;border-radius:0.5rem;">
|
||||
<p style="font-size:0.6rem;font-weight:700;text-transform:uppercase;color:#93c5fd;margin:0 0 0.25rem;">${t('grp_acaab_modal_note')}</p>
|
||||
<p style="font-size:0.875rem;color:#475569;font-style:italic;margin:0;">${escHtml(p.catatan)}</p>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<!-- Link + close buttons -->
|
||||
${linkSection}
|
||||
<button id="acaab-modal-close-btn" style="width:100%;padding:0.625rem;border:2px solid #e2e8f0;color:#475569;font-weight:700;background:white;border-radius:0.5rem;cursor:pointer;font-size:0.875rem;transition:border-color 0.2s,background 0.2s;">
|
||||
${t('grp_acaab_modal_close')}
|
||||
</button>
|
||||
</div>
|
||||
`, true);
|
||||
}
|
||||
|
||||
// ─── Modal shared ─────────────────────────────────────────────────────────────
|
||||
|
||||
function renderModal(innerHtml, hasCloseBtn = false) {
|
||||
const container = document.getElementById('acaab-modal');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="acaab-modal-backdrop" id="acaab-modal-bd">
|
||||
<div style="background:white;border-radius:0.75rem;max-width:672px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.35);">
|
||||
${innerHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
document.body.classList.add('acaab-modal-open');
|
||||
|
||||
document.getElementById('acaab-modal-close')?.addEventListener('click', closeModal);
|
||||
if (hasCloseBtn) {
|
||||
document.getElementById('acaab-modal-close-btn')?.addEventListener('click', closeModal);
|
||||
}
|
||||
document.getElementById('acaab-modal-bd').addEventListener('click', e => {
|
||||
if (e.target === e.currentTarget) closeModal();
|
||||
});
|
||||
document.addEventListener('keydown', onEsc);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const c = document.getElementById('acaab-modal');
|
||||
if (c) c.innerHTML = '';
|
||||
document.body.classList.remove('acaab-modal-open');
|
||||
document.removeEventListener('keydown', onEsc);
|
||||
}
|
||||
|
||||
function onEsc(e) { if (e.key === 'Escape') closeModal(); }
|
||||
|
||||
// ─── CTA / WhatsApp ───────────────────────────────────────────────────────────
|
||||
|
||||
const WA_NUMBER = '6285177655161';
|
||||
|
||||
function initWaButton() { updateWaButton(); }
|
||||
|
||||
function updateWaButton() {
|
||||
const btn = document.getElementById('acaab-cta-wa');
|
||||
if (!btn) return;
|
||||
const msg = getLang() === 'en'
|
||||
? 'Hello! I would like to know more about Informatika UNTAN student achievements and projects.'
|
||||
: 'Halo! Saya ingin mengetahui lebih lanjut tentang prestasi dan karya mahasiswa Informatika UNTAN.';
|
||||
btn.href = `https://wa.me/${WA_NUMBER}?text=${encodeURIComponent(msg)}`;
|
||||
}
|
||||
|
||||
// ─── Placeholder sync ─────────────────────────────────────────────────────────
|
||||
|
||||
function updateSearchPlaceholders() {
|
||||
const s1 = document.getElementById('acaab-ach-search');
|
||||
const s2 = document.getElementById('acaab-proj-search');
|
||||
if (s1) s1.placeholder = t('grp_acaab_ach_search');
|
||||
if (s2) s2.placeholder = t('grp_acaab_proj_search');
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "acaab-prestasi-karya",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { LanguageProvider } from './context/LanguageContext';
|
||||
import { DataProvider } from './context/DataContext';
|
||||
import Hero from './components/Hero';
|
||||
import Stats from './components/Stats';
|
||||
import AchievementGrid from './components/AchievementGrid';
|
||||
import ProjectCarousel from './components/ProjectCarousel';
|
||||
import CTA from './components/CTA';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<LanguageProvider>
|
||||
<DataProvider>
|
||||
{/* Breadcrumb */}
|
||||
<nav className="py-4" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center text-[10px] md:text-xs text-slate-500 font-medium tracking-wide uppercase">
|
||||
<li>
|
||||
<a href="https://informatika.untan.ac.id" target="_blank" rel="noopener noreferrer" className="hover:text-untan-blue transition-colors">
|
||||
Beranda
|
||||
</a>
|
||||
</li>
|
||||
<li><span className="mx-2 text-slate-300" aria-hidden="true">/</span></li>
|
||||
<li>
|
||||
<span className="text-slate-800 font-bold" aria-current="page">
|
||||
Mahasiswa & Prestasi
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<Hero />
|
||||
|
||||
<section className="acaab-full-bleed pb-20">
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8">
|
||||
<Stats />
|
||||
</div>
|
||||
<AchievementGrid />
|
||||
<ProjectCarousel />
|
||||
</section>
|
||||
|
||||
<CTA />
|
||||
</DataProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { Achievement } from '../data';
|
||||
import { Trophy, Calendar } from 'lucide-react';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
interface AchievementCardProps {
|
||||
achievement: Achievement;
|
||||
onReadMore: (achievement: Achievement) => void;
|
||||
}
|
||||
|
||||
export default function AchievementCard({ achievement, onReadMore }: AchievementCardProps) {
|
||||
const { t } = useLang();
|
||||
|
||||
return (
|
||||
<article className="bg-white rounded-lg overflow-hidden group hover:-translate-y-1 transition-all duration-300 shadow-md hover:shadow-xl border border-slate-200 flex flex-col h-full relative">
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-primary-navy z-10 group-hover:bg-primary-gold transition-colors"></div>
|
||||
|
||||
<div className="relative h-56 overflow-hidden bg-slate-100">
|
||||
<img
|
||||
src={achievement.image}
|
||||
alt={achievement.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = 'https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-3 left-3 bg-white/95 backdrop-blur text-primary-navy font-bold px-3 py-1 rounded shadow-sm flex items-center gap-1.5 text-xs">
|
||||
<Trophy size={14} className="text-primary-gold" />
|
||||
{achievement.rank}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 flex flex-col flex-grow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-untan-blue text-xs font-bold uppercase tracking-wider">
|
||||
{achievement.category}
|
||||
</span>
|
||||
<span className="text-slate-400 text-xs font-medium flex items-center gap-1 border-l border-slate-300 pl-3">
|
||||
<Calendar size={12} />
|
||||
{achievement.year}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold font-display text-primary-navy mb-3 leading-snug group-hover:text-untan-blue transition-colors">
|
||||
{achievement.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-slate-600 text-sm leading-relaxed mb-6 line-clamp-3">
|
||||
{achievement.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-auto border-t border-slate-100 pt-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded bg-primary-navy/5 flex items-center justify-center text-primary-navy font-bold text-xs ring-1 ring-primary-navy/10">
|
||||
{achievement.avatar}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-slate-800 block text-sm">{achievement.team}</span>
|
||||
<span className="text-slate-500 block text-xs">{achievement.generation}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="bg-slate-100 text-slate-600 text-[10px] px-2 py-1 rounded font-bold uppercase tracking-wide">
|
||||
{achievement.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onReadMore(achievement)}
|
||||
className="w-full py-2.5 rounded bg-primary-navy/5 text-primary-navy font-bold hover:bg-primary-navy hover:text-white transition-all duration-300 text-sm min-h-[44px]"
|
||||
>
|
||||
{t.achievement.readMore}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Filter, ChevronDown, Search, X } from 'lucide-react';
|
||||
import { type Achievement } from '../data';
|
||||
import { useData } from '../context/DataContext';
|
||||
import AchievementCard from './AchievementCard';
|
||||
import AchievementModal from './AchievementModal';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
const INITIAL_COUNT = 3;
|
||||
const LOAD_MORE_COUNT = 3;
|
||||
|
||||
export default function AchievementGrid() {
|
||||
const { t } = useLang();
|
||||
const { achievements, loading, error, refetch } = useData();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterLevel, setFilterLevel] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState('');
|
||||
const [filterYear, setFilterYear] = useState('');
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_COUNT);
|
||||
const [selectedAchievement, setSelectedAchievement] = useState<Achievement | null>(null);
|
||||
|
||||
// Derive unique categories and years from data
|
||||
const categories = useMemo(
|
||||
() => [...new Set(achievements.map((a) => a.category))].sort(),
|
||||
[achievements]
|
||||
);
|
||||
const years = useMemo(
|
||||
() => [...new Set(achievements.map((a) => a.year))].sort((a: number, b: number) => b - a),
|
||||
[achievements]
|
||||
);
|
||||
|
||||
// Filter + search logic
|
||||
const filtered = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
return achievements.filter((a) => {
|
||||
const matchSearch =
|
||||
!q ||
|
||||
a.title.toLowerCase().includes(q) ||
|
||||
a.team.toLowerCase().includes(q) ||
|
||||
a.category.toLowerCase().includes(q) ||
|
||||
a.competition.toLowerCase().includes(q);
|
||||
const matchLevel = !filterLevel || a.level === filterLevel;
|
||||
const matchCategory = !filterCategory || a.category === filterCategory;
|
||||
const matchYear = !filterYear || String(a.year) === filterYear;
|
||||
return matchSearch && matchLevel && matchCategory && matchYear;
|
||||
});
|
||||
}, [searchQuery, filterLevel, filterCategory, filterYear, achievements]);
|
||||
|
||||
const visible = filtered.slice(0, visibleCount);
|
||||
const hasMore = visibleCount < filtered.length;
|
||||
const isFiltered = searchQuery || filterLevel || filterCategory || filterYear;
|
||||
|
||||
const resetFilters = () => {
|
||||
setSearchQuery('');
|
||||
setFilterLevel('');
|
||||
setFilterCategory('');
|
||||
setFilterYear('');
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
};
|
||||
|
||||
const handleFilterChange = (setter: (v: string) => void) => (v: string) => {
|
||||
setter(v);
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => setVisibleCount((prev) => prev + LOAD_MORE_COUNT);
|
||||
|
||||
const handleShowLess = () => {
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
document.getElementById('achievement-grid-section')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Achievement Modal */}
|
||||
<AchievementModal achievement={selectedAchievement} onClose={() => setSelectedAchievement(null)} />
|
||||
|
||||
<section id="achievement-grid-section" className="py-8 bg-white">
|
||||
{/* Section header */}
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8 mb-8">
|
||||
<h2 className="text-3xl md:text-4xl font-black font-display text-primary-navy mb-2">
|
||||
{t.achievement.sectionTitle}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-base max-w-2xl">
|
||||
{t.achievement.sectionDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8">
|
||||
{/* Filter + Search Section */}
|
||||
<div className="bg-white/80 backdrop-blur-md p-4 rounded-lg shadow-sm border-t-4 border-primary-gold mb-8 space-y-3">
|
||||
{/* Row 1: Label + Reset */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-base font-bold font-display text-primary-navy flex items-center gap-2">
|
||||
<Filter className="text-primary-navy" size={18} />
|
||||
{t.achievement.filterTitle}
|
||||
</div>
|
||||
{isFiltered && (
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="flex items-center gap-1 text-xs font-bold text-slate-500 hover:text-primary-navy transition-colors"
|
||||
>
|
||||
<X size={13} />
|
||||
{t.achievement.resetFilter}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Search bar */}
|
||||
<div className="relative">
|
||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
<input
|
||||
id="search-achievement"
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => { setSearchQuery(e.target.value); setVisibleCount(INITIAL_COUNT); }}
|
||||
placeholder={t.achievement.searchPlaceholder}
|
||||
className="w-full bg-slate-50 text-slate-700 text-sm rounded py-2 pl-9 pr-4 border border-slate-200 focus:outline-none focus:ring-2 focus:ring-primary-gold"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => { setSearchQuery(''); setVisibleCount(INITIAL_COUNT); }}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
aria-label="Hapus pencarian"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Dropdowns */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{/* Level Filter */}
|
||||
<div className="relative flex-1 min-w-[130px]">
|
||||
<select
|
||||
id="filter-level"
|
||||
value={filterLevel}
|
||||
onChange={(e) => handleFilterChange(setFilterLevel)(e.target.value)}
|
||||
className="w-full bg-slate-50 text-slate-700 font-bold text-sm rounded py-2 pl-4 pr-9 shadow-sm focus:ring-2 focus:ring-primary-gold appearance-none cursor-pointer border border-slate-200"
|
||||
>
|
||||
<option value="">{t.achievement.allLevel}</option>
|
||||
<option value="Internasional">Internasional</option>
|
||||
<option value="Nasional">Nasional</option>
|
||||
<option value="Regional">Regional</option>
|
||||
</select>
|
||||
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="relative flex-1 min-w-[130px]">
|
||||
<select
|
||||
id="filter-category"
|
||||
value={filterCategory}
|
||||
onChange={(e) => handleFilterChange(setFilterCategory)(e.target.value)}
|
||||
className="w-full bg-slate-50 text-slate-700 font-bold text-sm rounded py-2 pl-4 pr-9 shadow-sm focus:ring-2 focus:ring-primary-gold appearance-none cursor-pointer border border-slate-200"
|
||||
>
|
||||
<option value="">{t.achievement.allField}</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Year Filter */}
|
||||
<div className="relative flex-1 min-w-[100px]">
|
||||
<select
|
||||
id="filter-year"
|
||||
value={filterYear}
|
||||
onChange={(e) => handleFilterChange(setFilterYear)(e.target.value)}
|
||||
className="w-full bg-slate-50 text-slate-700 font-bold text-sm rounded py-2 pl-4 pr-9 shadow-sm focus:ring-2 focus:ring-primary-gold appearance-none cursor-pointer border border-slate-200"
|
||||
>
|
||||
<option value="">{t.achievement.allYear}</option>
|
||||
{years.map((yr) => (
|
||||
<option key={yr} value={String(yr)}>{yr}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results count */}
|
||||
{isFiltered && (
|
||||
<p className="text-sm text-slate-500 font-medium mb-6">
|
||||
{t.achievement.showing}{' '}
|
||||
<span className="font-bold text-primary-navy">{filtered.length}</span>{' '}
|
||||
{t.achievement.matching}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Grid */}
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{[1, 2, 3].map((n) => (
|
||||
<div key={n} className="bg-white rounded-lg border border-slate-200 overflow-hidden shadow-sm animate-pulse">
|
||||
<div className="h-52 bg-slate-200 w-full" />
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="h-4 bg-slate-200 rounded w-1/4" />
|
||||
<div className="h-6 bg-slate-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-slate-200 rounded w-5/6" />
|
||||
<div className="h-4 bg-slate-200 rounded w-2/3" />
|
||||
<div className="h-10 bg-slate-200 rounded w-1/3 mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-20 text-red-500">
|
||||
<p className="text-lg font-semibold">Gagal memuat data</p>
|
||||
<p className="text-sm mt-1">{error}</p>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="mt-4 px-4 py-2 bg-primary-navy text-white font-bold rounded hover:bg-opacity-95 transition-all text-sm"
|
||||
>
|
||||
Coba Lagi
|
||||
</button>
|
||||
</div>
|
||||
) : visible.length === 0 ? (
|
||||
<div className="text-center py-20 text-slate-400">
|
||||
<p className="text-lg font-semibold">{t.achievement.noData}</p>
|
||||
<p className="text-sm mt-1">{t.achievement.noDataSub}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
{visible.map((achievement) => (
|
||||
<div key={achievement.id} className="animate-fade-in">
|
||||
<AchievementCard
|
||||
achievement={achievement}
|
||||
onReadMore={(a) => setSelectedAchievement(a)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load More / Show Less */}
|
||||
<div className="mt-20 flex flex-wrap items-center justify-center gap-6">
|
||||
{hasMore && (
|
||||
<button
|
||||
id="load-more-achievement"
|
||||
onClick={handleLoadMore}
|
||||
className="text-primary-navy font-black text-lg underline underline-offset-[12px] decoration-primary-gold decoration-4 hover:text-primary-gold transition-colors flex items-center gap-2"
|
||||
>
|
||||
{t.achievement.loadMore}
|
||||
<ChevronDown size={20} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!hasMore && visible.length > 0 && filtered.length > INITIAL_COUNT && (
|
||||
<p className="text-slate-400 text-sm font-medium">
|
||||
{t.achievement.showing} {filtered.length} {t.achievement.allShown}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{visibleCount > INITIAL_COUNT && visible.length > 0 && (
|
||||
<button
|
||||
onClick={handleShowLess}
|
||||
className="text-slate-400 font-bold text-sm underline underline-offset-4 decoration-slate-300 decoration-2 hover:text-slate-600 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<ChevronDown size={16} className="rotate-180" />
|
||||
{t.achievement.showLess}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { X, Trophy, Calendar, Award, Users, BookOpen, Tag } from 'lucide-react';
|
||||
import type { Achievement } from '../data';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
interface AchievementModalProps {
|
||||
achievement: Achievement | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex gap-2.5 items-start">
|
||||
<span className="text-primary-gold mt-0.5 shrink-0">{icon}</span>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 leading-none mb-1">{label}</p>
|
||||
<p className="text-sm font-semibold text-slate-800 leading-snug">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AchievementModal({ achievement, onClose }: AchievementModalProps) {
|
||||
const { t } = useLang();
|
||||
if (!achievement) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[300] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-label={achievement.title}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Image header */}
|
||||
<div className="relative h-56 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<img
|
||||
src={achievement.image}
|
||||
alt={achievement.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t.modal.close}
|
||||
className="absolute top-3 right-3 w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-md"
|
||||
>
|
||||
<X size={18} className="text-slate-700" />
|
||||
</button>
|
||||
|
||||
{/* Rank badge */}
|
||||
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||
<span className="bg-primary-gold text-slate-900 text-xs font-bold px-3 py-1.5 rounded flex items-center gap-1.5">
|
||||
<Trophy size={12} />
|
||||
{achievement.rank}
|
||||
</span>
|
||||
<span className="bg-white/20 backdrop-blur text-white text-xs font-bold px-3 py-1.5 rounded uppercase tracking-wide">
|
||||
{achievement.level}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<div className="mb-1">
|
||||
<span className="text-primary-untan-blue text-xs font-bold uppercase tracking-wider">
|
||||
{achievement.category}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-primary-navy leading-snug mb-5">{achievement.title}</h2>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg mb-5">
|
||||
<InfoRow icon={<Users size={14} />} label={t.modal.tim} value={achievement.team} />
|
||||
{achievement.nim && (
|
||||
<InfoRow icon={<BookOpen size={14} />} label={t.modal.nim} value={achievement.nim} />
|
||||
)}
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.angkatan} value={achievement.generation} />
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.tahun} value={String(achievement.year)} />
|
||||
<InfoRow icon={<Award size={14} />} label={t.modal.kompetisi} value={achievement.competition} />
|
||||
<InfoRow icon={<Tag size={14} />} label={t.modal.bidang} value={achievement.category} />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="border-t border-slate-100 pt-4">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">{t.modal.deskripsi}</p>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{achievement.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-6 w-full py-3 bg-primary-navy text-white font-bold rounded-lg hover:bg-untan-blue transition-colors duration-200 text-sm"
|
||||
>
|
||||
{t.modal.close}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MessageCircle } from 'lucide-react';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
const WA_NUMBER = '6285123456789'; // Ganti dengan nomor WhatsApp resmi
|
||||
|
||||
export default function CTA() {
|
||||
const { t, lang } = useLang();
|
||||
const waLink = `https://wa.me/${WA_NUMBER}?text=${encodeURIComponent(lang === 'id' ? 'Halo, saya ingin mendaftarkan prestasi atau karya saya.' : 'Hello, I would like to register my achievement or work.')}`;
|
||||
|
||||
return (
|
||||
<section className="acaab-full-bleed bg-untan-navy py-14 md:py-16 border-t border-white/10">
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8">
|
||||
<div className="bg-untan-yellow/10 border border-untan-yellow/20 rounded-2xl px-8 py-10 md:py-12 flex flex-col md:flex-row items-center justify-between gap-8">
|
||||
|
||||
{/* LEFT TEXT */}
|
||||
<div className="text-center md:text-left">
|
||||
<span className="inline-block bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 mb-4 rounded-sm">
|
||||
{t.cta.badge}
|
||||
</span>
|
||||
<h2 className="text-white text-2xl md:text-3xl font-bold font-serif leading-snug mb-3">
|
||||
{t.cta.title}
|
||||
</h2>
|
||||
<p className="text-slate-300 text-sm md:text-base leading-relaxed max-w-lg">
|
||||
{t.cta.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* RIGHT CTA */}
|
||||
<div className="flex flex-col items-center gap-3 shrink-0">
|
||||
<a
|
||||
href={waLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2.5 bg-[#25D366] hover:bg-[#20c05a] text-white text-sm font-bold px-6 py-3 rounded-full transition-all hover:scale-105 shadow-lg shadow-[#25D366]/20 whitespace-nowrap"
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
{t.cta.waButton}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useLang();
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-[#00243d] text-slate-300 py-16 border-t-[6px] border-untan-yellow">
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
|
||||
{/* Brand */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/id/0/03/Lambang_Universitas_Tanjungpura.png"
|
||||
alt="Logo UNTAN"
|
||||
className="w-12 h-12 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-bold font-display uppercase tracking-widest leading-tight text-white m-0">INFORMATIKA</span>
|
||||
<span className="text-[10px] tracking-[0.15em] uppercase text-untan-yellow">UNTAN PONTIANAK</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-medium leading-relaxed max-w-md text-slate-400 whitespace-pre-line">
|
||||
{t.footer.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Kategori */}
|
||||
<div>
|
||||
<h4 className="text-white font-bold text-sm uppercase mb-6 tracking-wide">{t.footer.kategori}</h4>
|
||||
<ul className="space-y-3 font-medium text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://informatika.untan.ac.id"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-untan-yellow transition-colors"
|
||||
>
|
||||
{t.footer.berita}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://informatika.untan.ac.id"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-untan-yellow transition-colors"
|
||||
>
|
||||
{t.footer.pengumuman}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#achievement-grid-section"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('achievement-grid-section')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
className="hover:text-untan-yellow transition-colors"
|
||||
>
|
||||
{t.footer.prestasi}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Tautan Penting */}
|
||||
<div>
|
||||
<h4 className="text-white font-bold text-sm uppercase mb-6 tracking-wide">{t.footer.tautan}</h4>
|
||||
<ul className="space-y-3 font-medium text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://siakad.untan.ac.id"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-untan-yellow transition-colors"
|
||||
>
|
||||
{t.footer.siakad}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://elearning.untan.ac.id"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-untan-yellow transition-colors"
|
||||
>
|
||||
{t.footer.elearning}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<div className="pt-8 border-t border-white/10 flex flex-col md:flex-row justify-between items-center gap-4 text-xs font-medium">
|
||||
<div>
|
||||
© {currentYear} Program Studi Informatika Universitas Tanjungpura. {t.footer.rights}
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<a href="#" className="hover:text-white transition-colors">{t.footer.privacy}</a>
|
||||
<a href="#" className="hover:text-white transition-colors">{t.footer.terms}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { Search, Menu, X, ChevronDown, Globe } from 'lucide-react';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { lang, t, toggleLang } = useLang();
|
||||
|
||||
const navLinks = [
|
||||
{ label: t.nav.tentangKami, hasDropdown: true, href: '#' },
|
||||
{ label: t.nav.program, hasDropdown: true, href: '#' },
|
||||
{ label: t.nav.sdm, hasDropdown: false, href: '#' },
|
||||
{ label: t.nav.fasilitas, hasDropdown: false, href: '#' },
|
||||
{ label: t.nav.karyaAgenda, hasDropdown: false, href: '#achievement-grid-section' },
|
||||
{ label: t.nav.kontak, hasDropdown: false, href: '#kontak' },
|
||||
];
|
||||
|
||||
const handleSmoothScroll = (e: MouseEvent<HTMLAnchorElement>, href: string) => {
|
||||
if (href.startsWith('#') && href.length > 1) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(href);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="nav-wrapper" className="fixed top-0 left-0 right-0 z-50 border-b border-slate-200 bg-white">
|
||||
{/* Top Dark Bar */}
|
||||
<div className="bg-untan-navy text-white py-4 lg:py-6 relative overflow-hidden hidden md:block">
|
||||
<div className="absolute inset-0 w-full overflow-hidden pointer-events-none" aria-hidden="true">
|
||||
<div className="absolute inset-0 opacity-[0.02] font-mono text-[10px] md:text-[12px] text-white select-none leading-snug whitespace-pre-wrap break-all text-right" style={{ overflow: 'hidden', maxHeight: '100%' }}>
|
||||
01101001 01101110 01100110 01101111 01110010 01101101 01100001 01110100 01101001 01101011 01100001
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-untan-navy via-untan-navy/80 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 lg:px-6 flex justify-between items-center relative z-10">
|
||||
<a href="#" className="flex items-center gap-4 group">
|
||||
<div className="relative w-16 h-16 lg:w-20 lg:h-20 flex items-center justify-center shrink-0">
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/id/0/03/Lambang_Universitas_Tanjungpura.png"
|
||||
alt="Logo UNTAN"
|
||||
className="w-full h-full object-contain transition-transform duration-500 group-hover:scale-105 group-hover:rotate-3 relative z-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="border-l-2 border-white/30 pl-4 lg:pl-5 py-1 flex flex-col justify-center">
|
||||
<span className="text-xl lg:text-3xl font-bold tracking-widest text-white leading-none font-sans drop-shadow-sm mb-1 uppercase">INFORMATIKA</span>
|
||||
<span className="text-[10px] lg:text-sm font-normal tracking-[0.15em] lg:tracking-[0.3em] text-slate-200 mt-0.5 opacity-90 drop-shadow-sm uppercase font-sans">UNIVERSITAS TANJUNGPURA</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-5">
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<a
|
||||
href="https://satu.untan.ac.id"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-untan-yellow transition-colors font-medium"
|
||||
>
|
||||
{t.nav.satuUntan}
|
||||
</a>
|
||||
</div>
|
||||
<div className="pl-5 border-l border-white/20 flex items-center gap-3">
|
||||
{/* Bilingual Toggle */}
|
||||
<button
|
||||
id="lang-toggle"
|
||||
onClick={toggleLang}
|
||||
aria-label={lang === 'id' ? 'Switch to English' : 'Ganti ke Bahasa Indonesia'}
|
||||
className="flex items-center gap-1.5 text-xs font-bold hover:text-untan-yellow transition-colors px-2 py-1 rounded hover:bg-white/10"
|
||||
>
|
||||
<Globe size={14} className="opacity-80" />
|
||||
<span className={lang === 'id' ? 'text-untan-yellow' : 'text-white/60'}>ID</span>
|
||||
<span className="text-white/30">|</span>
|
||||
<span className={lang === 'en' ? 'text-untan-yellow' : 'text-white/60'}>EN</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yellow Nav Bar */}
|
||||
<nav className="bg-untan-yellow shadow-md border-t border-white/20 relative" id="main-nav" aria-label="Navigasi utama">
|
||||
<div className="container mx-auto px-4 lg:px-6 flex items-center justify-between lg:justify-center w-full py-0 lg:py-2">
|
||||
|
||||
{/* Mobile: Logo */}
|
||||
<div className="flex items-center gap-2 lg:hidden py-3">
|
||||
<img
|
||||
src="https://upload.wikimedia.org/wikipedia/id/0/03/Lambang_Universitas_Tanjungpura.png"
|
||||
alt="Logo UNTAN"
|
||||
className="w-10 h-10 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col text-slate-800">
|
||||
<span className="text-base font-bold tracking-widest leading-none font-sans uppercase">INFORMATIKA</span>
|
||||
<span className="text-[8px] tracking-[0.15em] uppercase font-sans">UNTAN Pontianak</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<div className="hidden lg:flex items-center justify-center w-full gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
onClick={(e) => handleSmoothScroll(e, link.href)}
|
||||
className="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide"
|
||||
>
|
||||
<span>{link.label}</span>
|
||||
{link.hasDropdown && <ChevronDown size={12} className="opacity-70" />}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Mobile right: lang toggle + hamburger */}
|
||||
<div className="flex items-center gap-1 lg:hidden">
|
||||
<button
|
||||
id="lang-toggle-mobile"
|
||||
onClick={toggleLang}
|
||||
aria-label={lang === 'id' ? 'Switch to English' : 'Ganti ke Bahasa Indonesia'}
|
||||
className="flex items-center gap-1 text-[11px] font-bold text-slate-800 px-2 py-1 rounded hover:bg-black/10 transition-colors"
|
||||
>
|
||||
<Globe size={13} />
|
||||
<span>{lang.toUpperCase()}</span>
|
||||
</button>
|
||||
<button
|
||||
className="w-10 h-10 flex items-center justify-center text-slate-800 rounded-sm hover:bg-black/10 transition-colors"
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
aria-label="Toggle menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Dropdown Menu */}
|
||||
<div
|
||||
className={`lg:hidden overflow-hidden transition-all duration-300 ease-in-out ${isOpen ? 'max-h-screen opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
<div className="bg-untan-yellow border-t border-black/10 flex flex-col px-4 pb-4 pt-2 gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
onClick={(e) => handleSmoothScroll(e, link.href)}
|
||||
className="w-full text-left px-4 py-3 text-sm font-bold text-slate-800 hover:bg-black/5 rounded-sm transition-colors uppercase tracking-wide flex items-center justify-between"
|
||||
>
|
||||
{link.label}
|
||||
{link.hasDropdown && <ChevronDown size={14} className="opacity-50" />}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="h-[72px] lg:h-[190px]"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
export default function Hero() {
|
||||
const { t } = useLang();
|
||||
|
||||
return (
|
||||
<div
|
||||
id="hero"
|
||||
className="acaab-full-bleed bg-untan-navy text-white pt-6 md:pt-8 lg:pt-10 mb-8 border-t border-white/10 relative"
|
||||
>
|
||||
<div className="container mx-auto px-4 lg:px-8 relative z-10">
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-6 lg:gap-10 items-center">
|
||||
|
||||
{/* LEFT CONTENT */}
|
||||
<div className="max-w-3xl py-6 md:py-8 lg:py-10">
|
||||
<span className="inline-block bg-untan-yellow text-slate-900 text-[10px] md:text-xs font-bold uppercase tracking-widest px-2 py-0.5 mb-3">
|
||||
{t.hero.badge}
|
||||
</span>
|
||||
<h1 className="text-2xl md:text-3xl lg:text-[36px] font-bold font-serif leading-tight md:leading-snug tracking-tight mb-3">
|
||||
{t.hero.title}
|
||||
</h1>
|
||||
<p className="text-slate-300 max-w-2xl text-sm md:text-base leading-relaxed">
|
||||
{t.hero.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* RIGHT IMAGE */}
|
||||
<div className="relative flex justify-center lg:justify-end items-end">
|
||||
{/* Glow behind image */}
|
||||
<div className="absolute bottom-0 w-[300px] h-[300px] bg-cyan-300/10 rounded-full blur-3xl"></div>
|
||||
|
||||
<div className="relative">
|
||||
<img
|
||||
src={new URL('../gambar/hero.png', import.meta.url).href}
|
||||
alt="Mahasiswa Informatika UNTAN"
|
||||
className="
|
||||
relative z-10
|
||||
w-full
|
||||
max-w-[300px]
|
||||
md:max-w-[420px]
|
||||
lg:max-w-[450px]
|
||||
h-auto
|
||||
object-contain
|
||||
opacity-95
|
||||
drop-shadow-[0_15px_35px_rgba(0,0,0,0.25)]
|
||||
hover:scale-[1.02]
|
||||
transition-all
|
||||
duration-500
|
||||
"
|
||||
/>
|
||||
{/* Gradient fade bawah */}
|
||||
<div className="absolute bottom-0 left-0 w-full h-40 bg-gradient-to-t from-untan-navy to-transparent z-20" />
|
||||
{/* Gradient fade kiri */}
|
||||
<div className="absolute top-0 left-0 h-full w-20 bg-gradient-to-r from-untan-navy to-transparent z-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative Glow */}
|
||||
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-80 h-80 bg-untan-yellow/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { useData } from '../context/DataContext';
|
||||
import { useRef, useState, useMemo } from 'react';
|
||||
import { ArrowRight, ChevronLeft, ChevronRight, ChevronDown, User, Tag, Calendar, ExternalLink, Filter, Search, X } from 'lucide-react';
|
||||
import { type Project } from '../data';
|
||||
import ProjectModal from './ProjectModal';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
const INITIAL_COUNT = 6;
|
||||
const LOAD_MORE_COUNT = 3;
|
||||
|
||||
|
||||
|
||||
export default function ProjectCarousel() {
|
||||
const { t } = useLang();
|
||||
const { projects, loading, error, refetch } = useData();
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_COUNT);
|
||||
|
||||
// Filter state
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState('');
|
||||
const [filterYear, setFilterYear] = useState('');
|
||||
|
||||
const categories = useMemo(
|
||||
() => [...new Set(projects.map((p) => p.category))].sort(),
|
||||
[projects]
|
||||
);
|
||||
|
||||
const years = useMemo(
|
||||
() => [...new Set(projects.map((p) => p.year))].sort((a: number, b: number) => b - a),
|
||||
[projects]
|
||||
);
|
||||
|
||||
// Filter + search logic
|
||||
const filtered = useMemo(() => {
|
||||
const q = searchQuery.toLowerCase().trim();
|
||||
return projects.filter((p) => {
|
||||
const matchSearch =
|
||||
!q ||
|
||||
p.title.toLowerCase().includes(q) ||
|
||||
p.studentName.toLowerCase().includes(q) ||
|
||||
p.category.toLowerCase().includes(q) ||
|
||||
p.description.toLowerCase().includes(q);
|
||||
const matchCategory = !filterCategory || p.category === filterCategory;
|
||||
const matchYear = !filterYear || String(p.year) === filterYear;
|
||||
return matchSearch && matchCategory && matchYear;
|
||||
});
|
||||
}, [searchQuery, filterCategory, filterYear, projects]);
|
||||
|
||||
const visibleProjects = filtered.slice(0, visibleCount);
|
||||
const hasMore = visibleCount < filtered.length;
|
||||
const isFiltered = searchQuery || filterCategory || filterYear;
|
||||
|
||||
const resetFilters = () => {
|
||||
setSearchQuery('');
|
||||
setFilterCategory('');
|
||||
setFilterYear('');
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
};
|
||||
|
||||
const handleFilterChange = (setter: (v: string) => void) => (v: string) => {
|
||||
setter(v);
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => setVisibleCount((prev) => prev + LOAD_MORE_COUNT);
|
||||
|
||||
const handleShowLess = () => {
|
||||
setVisibleCount(INITIAL_COUNT);
|
||||
document.getElementById('portofolio')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
const scroll = (direction: 'left' | 'right') => {
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
const cardWidth = container.firstElementChild
|
||||
? (container.firstElementChild as HTMLElement).offsetWidth + 24
|
||||
: 400;
|
||||
container.scrollBy({ left: direction === 'right' ? cardWidth : -cardWidth, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Project Modal */}
|
||||
<ProjectModal project={selectedProject} onClose={() => setSelectedProject(null)} />
|
||||
|
||||
<section id="portofolio" className="py-16 bg-white relative overflow-hidden border-t border-slate-100">
|
||||
<div className="max-w-7xl mx-auto px-4 lg:px-8">
|
||||
{/* Header row */}
|
||||
<div className="flex items-start justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h2 className="text-3xl md:text-4xl font-black font-display text-primary-navy mb-3">
|
||||
{t.project.sectionTitle}
|
||||
</h2>
|
||||
<p className="text-base text-slate-500 font-medium max-w-2xl">
|
||||
{t.project.sectionDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Scroll control buttons */}
|
||||
<div className="flex items-center gap-2 shrink-0 mt-1">
|
||||
<button
|
||||
onClick={() => scroll('left')}
|
||||
aria-label="Geser kiri"
|
||||
className="w-10 h-10 rounded-full border-2 border-primary-navy/20 flex items-center justify-center text-primary-navy hover:bg-primary-navy hover:text-white hover:border-primary-navy transition-all duration-200 shadow-sm"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => scroll('right')}
|
||||
aria-label="Geser kanan"
|
||||
className="w-10 h-10 rounded-full border-2 border-primary-navy/20 flex items-center justify-center text-primary-navy hover:bg-primary-navy hover:text-white hover:border-primary-navy transition-all duration-200 shadow-sm"
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Filter + Search Section */}
|
||||
<div className="bg-white/80 backdrop-blur-md p-4 rounded-lg shadow-sm border-t-4 border-primary-gold mb-8 space-y-3">
|
||||
{/* Row 1: Label + Reset */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-base font-bold font-display text-primary-navy flex items-center gap-2">
|
||||
<Filter className="text-primary-navy" size={18} />
|
||||
{t.project.filterTitle}
|
||||
</div>
|
||||
{isFiltered && (
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="flex items-center gap-1 text-xs font-bold text-slate-500 hover:text-primary-navy transition-colors"
|
||||
>
|
||||
<X size={13} />
|
||||
{t.project.resetFilter}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Search bar */}
|
||||
<div className="relative">
|
||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
<input
|
||||
id="search-project"
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => { setSearchQuery(e.target.value); setVisibleCount(INITIAL_COUNT); }}
|
||||
placeholder={t.project.searchPlaceholder}
|
||||
className="w-full bg-slate-50 text-slate-700 text-sm rounded py-2 pl-9 pr-4 border border-slate-200 focus:outline-none focus:ring-2 focus:ring-primary-gold"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => { setSearchQuery(''); setVisibleCount(INITIAL_COUNT); }}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
||||
aria-label="Hapus pencarian"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Dropdowns */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{/* Category Filter */}
|
||||
<div className="relative flex-1 min-w-[160px]">
|
||||
<select
|
||||
id="filter-project-category"
|
||||
value={filterCategory}
|
||||
onChange={(e) => handleFilterChange(setFilterCategory)(e.target.value)}
|
||||
className="w-full bg-slate-50 text-slate-700 font-bold text-sm rounded py-2 pl-4 pr-9 shadow-sm focus:ring-2 focus:ring-primary-gold appearance-none cursor-pointer border border-slate-200"
|
||||
>
|
||||
<option value="">{t.project.allField}</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Year Filter */}
|
||||
<div className="relative flex-1 min-w-[120px]">
|
||||
<select
|
||||
id="filter-project-year"
|
||||
value={filterYear}
|
||||
onChange={(e) => handleFilterChange(setFilterYear)(e.target.value)}
|
||||
className="w-full bg-slate-50 text-slate-700 font-bold text-sm rounded py-2 pl-4 pr-9 shadow-sm focus:ring-2 focus:ring-primary-gold appearance-none cursor-pointer border border-slate-200"
|
||||
>
|
||||
<option value="">{t.project.allYear}</option>
|
||||
{years.map((yr) => (
|
||||
<option key={yr} value={String(yr)}>{yr}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results count */}
|
||||
{isFiltered && (
|
||||
<p className="text-sm text-slate-500 font-medium mb-6">
|
||||
{t.project.showing}{' '}
|
||||
<span className="font-bold text-primary-navy">{filtered.length}</span>{' '}
|
||||
{t.project.matching}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Cards */}
|
||||
{loading ? (
|
||||
<div className="flex gap-6 overflow-x-auto pb-10 hide-scrollbar">
|
||||
{[1, 2, 3].map((n) => (
|
||||
<div
|
||||
key={n}
|
||||
className="snap-start shrink-0 proj-card bg-white rounded-lg border border-slate-200 overflow-hidden shadow-md animate-pulse flex flex-col"
|
||||
>
|
||||
<div className="h-52 bg-slate-200 w-full" />
|
||||
<div className="p-5 space-y-4 flex-grow flex flex-col justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 bg-slate-200 rounded w-1/4" />
|
||||
<div className="h-5 bg-slate-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-slate-200 rounded w-5/6" />
|
||||
<div className="h-4 bg-slate-200 rounded w-2/3" />
|
||||
</div>
|
||||
<div className="h-10 bg-slate-200 rounded w-1/3 mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-10 text-red-500 w-full">
|
||||
<p className="text-lg font-semibold">Gagal memuat portofolio</p>
|
||||
<p className="text-sm mt-1">{error}</p>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="mt-4 px-4 py-2 bg-primary-navy text-white font-bold rounded hover:bg-opacity-95 transition-all text-sm"
|
||||
>
|
||||
Coba Lagi
|
||||
</button>
|
||||
</div>
|
||||
) : visibleProjects.length === 0 ? (
|
||||
<div className="text-center py-20 text-slate-400">
|
||||
<p className="text-lg font-semibold">{t.project.noData}</p>
|
||||
<p className="text-sm mt-1">{t.project.noDataSub}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Scrollable cards */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex gap-6 overflow-x-auto pb-10 hide-scrollbar snap-x snap-mandatory"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{visibleProjects.map((project) => (
|
||||
<div
|
||||
key={project.id}
|
||||
className="snap-start shrink-0 proj-card bg-white rounded-lg overflow-hidden shadow-md border border-slate-200 group flex flex-col relative"
|
||||
>
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-primary-gold z-10 transition-transform origin-bottom group-hover:scale-y-110" />
|
||||
|
||||
{/* Image */}
|
||||
<div className="h-52 overflow-hidden relative">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).src = 'https://placehold.co/800x600/003150/FDB813?text=Informatika+UNTAN';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-primary-navy/80 to-transparent p-5 flex flex-col justify-end">
|
||||
<h4 className="text-lg font-bold font-display text-white leading-snug mb-1">
|
||||
{project.title}
|
||||
</h4>
|
||||
{project.category && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-primary-gold uppercase tracking-wider">
|
||||
<Tag size={10} />
|
||||
{project.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-5 flex flex-col flex-grow">
|
||||
{/* Meta row */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-3 text-xs text-slate-500 font-medium">
|
||||
{project.studentName && (
|
||||
<span className="flex items-center gap-1">
|
||||
<User size={11} className="text-slate-400" />
|
||||
{project.studentName}
|
||||
</span>
|
||||
)}
|
||||
{project.year && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar size={11} className="text-slate-400" />
|
||||
{project.year}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-slate-600 text-sm leading-relaxed mb-5 flex-grow line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 mt-auto">
|
||||
<button
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="text-primary-navy font-bold text-sm tracking-wide hover:text-untan-blue flex items-center gap-2 group/btn"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
{t.project.readMore}
|
||||
<ArrowRight size={15} className="group-hover/btn:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
|
||||
{project.link && (
|
||||
<a
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-auto flex items-center gap-1.5 text-xs font-bold text-slate-500 hover:text-primary-navy transition-colors border border-slate-200 hover:border-primary-navy/30 rounded px-3 py-2"
|
||||
style={{ minHeight: '44px' }}
|
||||
>
|
||||
<ExternalLink size={13} />
|
||||
{t.project.visitLink}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Scroll hint */}
|
||||
<p className="text-center text-xs text-slate-400 font-medium mt-2 select-none">
|
||||
{t.project.scrollHint}
|
||||
</p>
|
||||
|
||||
{/* Load More / Show Less */}
|
||||
<div className="mt-10 flex flex-wrap items-center justify-center gap-6">
|
||||
{hasMore && (
|
||||
<button
|
||||
id="load-more-project"
|
||||
onClick={handleLoadMore}
|
||||
className="text-primary-navy font-black text-base underline underline-offset-[10px] decoration-primary-gold decoration-4 hover:text-primary-gold transition-colors flex items-center gap-2"
|
||||
>
|
||||
{t.project.loadMore}
|
||||
<ChevronDown size={18} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!hasMore && visibleProjects.length > 0 && filtered.length > INITIAL_COUNT && (
|
||||
<p className="text-slate-400 text-sm font-medium">
|
||||
{t.project.showing} {filtered.length} {t.project.allShown}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{visibleCount > INITIAL_COUNT && visibleProjects.length > 0 && (
|
||||
<button
|
||||
onClick={handleShowLess}
|
||||
className="text-slate-400 font-bold text-sm underline underline-offset-4 decoration-slate-300 decoration-2 hover:text-slate-600 transition-colors flex items-center gap-1.5"
|
||||
>
|
||||
<ChevronDown size={16} className="rotate-180" />
|
||||
{t.project.showLess}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { X, ExternalLink, Users, BookOpen, Calendar, Tag, FileText } from 'lucide-react';
|
||||
import type { Project } from '../data';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
|
||||
interface ProjectModalProps {
|
||||
project: Project | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex gap-2.5 items-start">
|
||||
<span className="text-primary-gold mt-0.5 shrink-0">{icon}</span>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 leading-none mb-1">{label}</p>
|
||||
<p className="text-sm font-semibold text-slate-800 leading-snug">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProjectModal({ project, onClose }: ProjectModalProps) {
|
||||
const { t } = useLang();
|
||||
if (!project) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[300] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-modal="true"
|
||||
role="dialog"
|
||||
aria-label={project.title}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-fade-in"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Image header */}
|
||||
<div className="relative h-56 overflow-hidden rounded-t-xl bg-slate-200 shrink-0">
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label={t.modal.close}
|
||||
className="absolute top-3 right-3 w-9 h-9 bg-white/90 backdrop-blur rounded-full flex items-center justify-center hover:bg-white transition-colors shadow-md"
|
||||
>
|
||||
<X size={18} className="text-slate-700" />
|
||||
</button>
|
||||
|
||||
{/* Category badge */}
|
||||
{project.category && (
|
||||
<div className="absolute bottom-4 left-4">
|
||||
<span className="bg-primary-gold text-slate-900 text-xs font-bold px-3 py-1.5 rounded flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<Tag size={12} />
|
||||
{project.category}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold text-primary-navy leading-snug mb-5">{project.title}</h2>
|
||||
|
||||
{/* Info grid */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 bg-slate-50 rounded-lg mb-5">
|
||||
{project.studentName && (
|
||||
<InfoRow icon={<Users size={14} />} label={t.modal.mahasiswa} value={project.studentName} />
|
||||
)}
|
||||
{project.nim && (
|
||||
<InfoRow icon={<BookOpen size={14} />} label={t.modal.nim} value={project.nim} />
|
||||
)}
|
||||
{project.year && (
|
||||
<InfoRow icon={<Calendar size={14} />} label={t.modal.tahun} value={String(project.year)} />
|
||||
)}
|
||||
{project.category && (
|
||||
<InfoRow icon={<Tag size={14} />} label={t.modal.bidang} value={project.category} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="border-t border-slate-100 pt-4 mb-5">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">{t.modal.deskripsi}</p>
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{project.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Project link */}
|
||||
{project.link ? (
|
||||
<a
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full py-3 bg-primary-navy text-white font-bold rounded-lg hover:bg-untan-blue transition-colors duration-200 text-sm mb-3"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
{t.project.visitLink}
|
||||
</a>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-2 w-full py-3 bg-slate-100 text-slate-400 font-bold rounded-lg text-sm mb-3 cursor-not-allowed">
|
||||
<FileText size={16} />
|
||||
{t.project.noLink}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full py-2.5 border-2 border-slate-200 text-slate-600 font-bold rounded-lg hover:border-slate-300 hover:bg-slate-50 transition-colors duration-200 text-sm"
|
||||
>
|
||||
{t.modal.close}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLang } from '../context/LanguageContext';
|
||||
import { useData } from '../context/DataContext';
|
||||
import { BookOpen, Award, Globe2 } from 'lucide-react';
|
||||
|
||||
/* ── Animated counter hook ─────────────────────────────────────────────── */
|
||||
function useCountUp(target: number, duration = 1600) {
|
||||
const [count, setCount] = useState(0);
|
||||
const elRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggered = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
triggered.current = false;
|
||||
let animationFrameId: number;
|
||||
let observer: IntersectionObserver;
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !triggered.current) {
|
||||
triggered.current = true;
|
||||
|
||||
let startTimestamp: number | null = null;
|
||||
|
||||
const step = (timestamp: number) => {
|
||||
if (!startTimestamp) startTimestamp = timestamp;
|
||||
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
|
||||
|
||||
// easeOutQuart easing function
|
||||
const easeProgress = 1 - Math.pow(1 - progress, 4);
|
||||
|
||||
setCount(Math.round(target * easeProgress));
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
} else {
|
||||
setCount(target);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(step);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.4 },
|
||||
);
|
||||
|
||||
if (elRef.current) observer.observe(elRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [target, duration]);
|
||||
|
||||
return { count, elRef };
|
||||
}
|
||||
|
||||
/* ── Stat number with label ─────────────────────────────────────────────── */
|
||||
function StatItem({ value, label, delay = 0 }: { value: number; label: string; delay?: number }) {
|
||||
const { count, elRef } = useCountUp(value, 1600 + delay);
|
||||
return (
|
||||
<div ref={elRef} className="flex flex-col items-center gap-1" style={{ minWidth: 'clamp(5rem, 10vw, 8rem)' }}>
|
||||
<div
|
||||
className="font-display font-black text-primary-navy leading-none tabular-nums"
|
||||
style={{ fontSize: 'clamp(2.8rem, 6vw, 5rem)' }}
|
||||
>
|
||||
{count}
|
||||
</div>
|
||||
<div
|
||||
className="font-display font-bold text-primary-navy text-center leading-tight uppercase tracking-wide"
|
||||
style={{ fontSize: 'clamp(0.6rem, 1.1vw, 0.8rem)' }}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Section panel ──────────────────────────────────────────────────────── */
|
||||
function StatPanel({
|
||||
title,
|
||||
children,
|
||||
flexClass = 'flex-1',
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
flexClass?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={`flex flex-col gap-5 ${flexClass}`}>
|
||||
{/* Dark pill header — full width of this panel */}
|
||||
<div
|
||||
className="bg-primary-navy text-white font-display font-bold text-center rounded-xl px-8 py-3 w-full uppercase tracking-wide"
|
||||
style={{ fontSize: 'clamp(0.85rem, 1.5vw, 1.05rem)' }}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{/* Numbers row — equal spacing */}
|
||||
<div className="flex items-end justify-evenly pb-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Main export ────────────────────────────────────────────────────────── */
|
||||
export default function Stats() {
|
||||
const { t, lang } = useLang();
|
||||
const { achievements, projects } = useData();
|
||||
const totalPrestasi = achievements.length;
|
||||
const totalProyek = projects.length;
|
||||
const internasional = achievements.filter(a => a.level === 'Internasional').length;
|
||||
const nasional = achievements.filter(a => a.level === 'Nasional').length;
|
||||
const regional = achievements.filter(a => a.level === 'Regional').length;
|
||||
|
||||
const today = new Date().toLocaleDateString(
|
||||
lang === 'id' ? 'id-ID' : 'en-US',
|
||||
{ day: 'numeric', month: 'long', year: 'numeric' }
|
||||
);
|
||||
const dataNote = lang === 'id'
|
||||
? `Data Terkumpul Per ${today}`
|
||||
: `Data Collected As Of ${today}`;
|
||||
|
||||
return (
|
||||
<div id="tentang">
|
||||
{/* ── Stats bar ─────────────────────────────────────────────────── */}
|
||||
<section className="relative z-10 mb-2 rounded-xl overflow-hidden shadow-[0_10px_30px_-10px_rgba(253,184,19,0.4)]">
|
||||
<div className="bg-[#FDB813] relative px-6 md:px-10 py-7 md:py-9">
|
||||
<div className="absolute inset-0 bg-white/5 mix-blend-overlay pointer-events-none" />
|
||||
|
||||
<div className="relative flex flex-col md:flex-row md:items-start gap-8 md:gap-6">
|
||||
|
||||
{/* ① Total Prestasi — 3 kolom → flex-[3] */}
|
||||
<StatPanel
|
||||
title={lang === 'id' ? 'Total Prestasi' : 'Total Achievements'}
|
||||
flexClass="flex-[4]"
|
||||
>
|
||||
<StatItem value={totalPrestasi} label={lang === 'id' ? 'prestasi mahasiswa' : 'student achievements'} delay={0} />
|
||||
<StatItem value={internasional} label={lang === 'id' ? 'internasional' : 'international'} delay={150} />
|
||||
<StatItem value={nasional} label={lang === 'id' ? 'nasional' : 'national'} delay={300} />
|
||||
<StatItem value={regional} label="regional" delay={450} />
|
||||
</StatPanel>
|
||||
|
||||
{/* ② Total Portofolio — 1 kolom → flex-[1] */}
|
||||
<StatPanel
|
||||
title={lang === 'id' ? 'Total Portofolio' : 'Total Portfolio'}
|
||||
flexClass="flex-[1.5]"
|
||||
>
|
||||
<StatItem value={totalProyek} label={lang === 'id' ? 'proyek mahasiswa' : 'student projects'} delay={200} />
|
||||
</StatPanel>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keterangan tanggal — di luar kotak, rata kanan */}
|
||||
<p className="text-[9px] md:text-[10px] font-semibold text-slate-400 tracking-wide mb-16 text-right pr-1">
|
||||
{dataNote}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Fact card helper ───────────────────────────────────────────────────── */
|
||||
function FactCard({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 bg-slate-50 rounded-lg px-4 py-3 border border-slate-100">
|
||||
<span className="text-primary-gold">{icon}</span>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold uppercase tracking-wide text-slate-400">{label}</p>
|
||||
<p className="text-sm font-bold text-primary-navy">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { Achievement, Project } from '../data';
|
||||
import { fetchAchievementsFromAPI, fetchProjectsFromAPI } from '../services/api';
|
||||
|
||||
interface DataContextType {
|
||||
achievements: Achievement[];
|
||||
projects: Project[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||
|
||||
export function DataProvider({ children }: { children: ReactNode }) {
|
||||
const [achievements, setAchievements] = useState<Achievement[]>([]);
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
fetchAchievementsFromAPI(),
|
||||
fetchProjectsFromAPI(),
|
||||
]);
|
||||
|
||||
const achievementsRes = results[0];
|
||||
const projectsRes = results[1];
|
||||
|
||||
let hasError = false;
|
||||
let errorMsgs: string[] = [];
|
||||
|
||||
if (achievementsRes.status === 'fulfilled') {
|
||||
setAchievements(achievementsRes.value);
|
||||
} else {
|
||||
console.error('Failed to fetch achievements:', achievementsRes.reason);
|
||||
errorMsgs.push('Gagal memuat data prestasi.');
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (projectsRes.status === 'fulfilled') {
|
||||
setProjects(projectsRes.value);
|
||||
} else {
|
||||
console.error('Failed to fetch projects:', projectsRes.reason);
|
||||
errorMsgs.push('Gagal memuat data portofolio.');
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
setError(errorMsgs.join(' '));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Unexpected error loading API data:', err);
|
||||
setError('Terjadi kesalahan yang tidak terduga saat memuat data.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataContext.Provider
|
||||
value={{
|
||||
achievements,
|
||||
projects,
|
||||
loading,
|
||||
error,
|
||||
refetch: loadData,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useData() {
|
||||
const context = useContext(DataContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useData must be used within a DataProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
||||
import { translations, type Lang, type Translations } from '../i18n';
|
||||
|
||||
const KIT_STORAGE_KEY = 'if_untan_lang';
|
||||
|
||||
function readKitLang(): Lang {
|
||||
const saved = localStorage.getItem(KIT_STORAGE_KEY);
|
||||
if (saved === 'en') return 'en';
|
||||
const browser = (navigator.language || 'id').toLowerCase();
|
||||
return browser.startsWith('en') ? 'en' : 'id';
|
||||
}
|
||||
|
||||
interface LanguageContextType {
|
||||
lang: Lang;
|
||||
t: Translations;
|
||||
toggleLang: () => void;
|
||||
}
|
||||
|
||||
const LanguageContext = createContext<LanguageContextType | null>(null);
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLang] = useState<Lang>(readKitLang);
|
||||
|
||||
// Sync dengan kit's language toggle (tombol ID/EN di navbar)
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<{ lang: string }>).detail;
|
||||
if (detail?.lang === 'en' || detail?.lang === 'id') {
|
||||
setLang(detail.lang as Lang);
|
||||
}
|
||||
};
|
||||
document.addEventListener('languageChanged', handler);
|
||||
return () => document.removeEventListener('languageChanged', handler);
|
||||
}, []);
|
||||
|
||||
const toggleLang = () => setLang((prev) => (prev === 'id' ? 'en' : 'id'));
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={{ lang, t: translations[lang], toggleLang }}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLang() {
|
||||
const ctx = useContext(LanguageContext);
|
||||
if (!ctx) throw new Error('useLang must be used within LanguageProvider');
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
export interface Achievement {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
competition: string;
|
||||
rank: string;
|
||||
year: number;
|
||||
level: 'Nasional' | 'Internasional' | 'Regional';
|
||||
description: string;
|
||||
team: string;
|
||||
nim?: string;
|
||||
generation: string;
|
||||
image: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
studentName: string;
|
||||
nim?: string;
|
||||
year: number;
|
||||
category: string;
|
||||
description: string;
|
||||
image: string;
|
||||
link?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
export const ACHIEVEMENTS: Achievement[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Gemastik XVI: Smart Agriculture AI System',
|
||||
category: 'Kecerdasan Buatan',
|
||||
competition: 'Gemastik XVI',
|
||||
rank: 'Juara 1',
|
||||
year: 2023,
|
||||
level: 'Nasional',
|
||||
description: 'Sistem deteksi dini penyakit tanaman menggunakan Computer Vision dan Deep Learning untuk membantu petani meningkatkan hasil panen secara berkelanjutan.',
|
||||
team: 'Tim Bima Sakti',
|
||||
nim: 'H1031201001',
|
||||
generation: 'Angkatan 2020',
|
||||
image: 'https://picsum.photos/seed/ai-agri/800/600',
|
||||
avatar: 'BS',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'ASEAN Cyber Shield Hackathon',
|
||||
category: 'Cyber Security',
|
||||
competition: 'ASEAN Cyber Shield',
|
||||
rank: '10 Besar',
|
||||
year: 2023,
|
||||
level: 'Internasional',
|
||||
description: 'Mengembangkan solusi pertahanan siber proaktif terhadap ancaman zero-day dalam kompetisi keamanan siber tingkat regional Asia Tenggara.',
|
||||
team: 'DefendUntan',
|
||||
nim: 'H1031211042',
|
||||
generation: 'Angkatan 2021',
|
||||
image: 'https://picsum.photos/seed/cyber/800/600',
|
||||
avatar: 'DU',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Innovation in Image Processing for Healthcare',
|
||||
category: 'Kecerdasan Buatan',
|
||||
competition: 'PIMNAS 36',
|
||||
rank: 'Medali Emas',
|
||||
year: 2024,
|
||||
level: 'Nasional',
|
||||
description: 'Riset mengenai otomatisasi identifikasi sel kanker melalui citra mikroskopis menggunakan arsitektur Neural Network yang dioptimalkan.',
|
||||
team: 'Alpha Medical',
|
||||
nim: 'H1031221015',
|
||||
generation: 'Angkatan 2022',
|
||||
image: 'https://picsum.photos/seed/medical/800/600',
|
||||
avatar: 'AM',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Smart Waste Management – IOT Challenge',
|
||||
category: 'IoT',
|
||||
competition: 'IOT Challenge ITS 2024',
|
||||
rank: 'Juara 2',
|
||||
year: 2024,
|
||||
level: 'Nasional',
|
||||
description: 'Sistem pengelolaan sampah cerdas berbasis sensor IoT yang mampu mendeteksi kapasitas tempat sampah dan mengoptimalkan rute pengangkutan sampah kota.',
|
||||
team: 'Tim GreenTech',
|
||||
nim: 'H1031211058',
|
||||
generation: 'Angkatan 2021',
|
||||
image: 'https://picsum.photos/seed/waste/800/600',
|
||||
avatar: 'GT',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'UXID National Design Challenge',
|
||||
category: 'Software Engineering',
|
||||
competition: 'UXID National Challenge',
|
||||
rank: 'Finalis Top 5',
|
||||
year: 2023,
|
||||
level: 'Nasional',
|
||||
description: 'Desain antarmuka aplikasi kesehatan mental yang berpusat pada pengguna remaja, mengintegrasikan prinsip aksesibilitas dan human-centered design.',
|
||||
team: 'Tim Pixel Prima',
|
||||
nim: 'H1031221033',
|
||||
generation: 'Angkatan 2022',
|
||||
image: 'https://picsum.photos/seed/ux/800/600',
|
||||
avatar: 'PP',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Pekan Ilmiah Mahasiswa – Lomba Antar Kampus Kalbar',
|
||||
category: 'Software Engineering',
|
||||
competition: 'PIM Kalbar 2024',
|
||||
rank: 'Juara 1',
|
||||
year: 2024,
|
||||
level: 'Regional',
|
||||
description: 'Aplikasi sistem informasi keuangan UMKM berbasis cloud yang memudahkan pelaku usaha kecil menengah di Kalimantan Barat dalam pengelolaan keuangan digital.',
|
||||
team: 'Tim Borneo Dev',
|
||||
nim: 'H1031231005',
|
||||
generation: 'Angkatan 2023',
|
||||
image: 'https://picsum.photos/seed/finance/800/600',
|
||||
avatar: 'BD',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'International AI for Good Hackathon – UNESCO',
|
||||
category: 'Kecerdasan Buatan',
|
||||
competition: 'AI for Good UNESCO',
|
||||
rank: 'Top 10 Global',
|
||||
year: 2024,
|
||||
level: 'Internasional',
|
||||
description: 'Solusi AI untuk deteksi deforestasi hutan tropis secara real-time menggunakan citra satelit dan machine learning, mendukung pelestarian lingkungan global.',
|
||||
team: 'Tim EarthAI',
|
||||
nim: 'H1031201022',
|
||||
generation: 'Angkatan 2020',
|
||||
image: 'https://picsum.photos/seed/deforest/800/600',
|
||||
avatar: 'EA',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Kompetisi Antar Kampus – Data Science Cup Untan',
|
||||
category: 'Data',
|
||||
competition: 'Data Science Cup 2023',
|
||||
rank: 'Juara 2',
|
||||
year: 2023,
|
||||
level: 'Regional',
|
||||
description: 'Analisis prediktif kepadatan lalu lintas Kota Pontianak menggunakan model time-series dan visualisasi data interaktif untuk mendukung kebijakan transportasi kota.',
|
||||
team: 'Tim DataNesia',
|
||||
nim: 'H1031211077',
|
||||
generation: 'Angkatan 2021',
|
||||
image: 'https://picsum.photos/seed/datascience/800/600',
|
||||
avatar: 'DN',
|
||||
},
|
||||
];
|
||||
|
||||
export const PROJECTS: Project[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
title: 'IoT Environment Monitoring System',
|
||||
studentName: 'Reza Firmansyah',
|
||||
nim: 'H1031201031',
|
||||
year: 2023,
|
||||
category: 'IoT',
|
||||
description: 'Platform pemantauan kualitas udara dan tanah secara real-time di area gambut Kalimantan Barat menggunakan sensor terdistribusi.',
|
||||
image: 'https://picsum.photos/seed/iot/800/600',
|
||||
link: 'https://github.com/informatika-untan/iot-env-monitor',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: 'Smart City Data Analytics',
|
||||
studentName: 'Anisa Putri Rahmadani',
|
||||
nim: 'H1031211019',
|
||||
year: 2023,
|
||||
category: 'Data',
|
||||
description: 'Visualisasi dan analisis mobilitas kendaraan di Kota Pontianak untuk mengoptimalkan manajemen lalu lintas perkotaan.',
|
||||
image: 'https://picsum.photos/seed/analytics/800/600',
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
title: 'Open Education Platform',
|
||||
studentName: 'Dimas Aryo Wicaksono',
|
||||
nim: 'H1031221044',
|
||||
year: 2024,
|
||||
category: 'Software Engineering',
|
||||
description: 'Sistem manajemen pembelajaran inklusif yang dirancang khusus untuk memenuhi standar kurikulum berbasis OBE (Outcome-Based Education).',
|
||||
image: 'https://picsum.photos/seed/edu/800/600',
|
||||
link: 'https://github.com/informatika-untan/open-edu',
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
title: 'AI-Powered Crop Disease Detector',
|
||||
studentName: 'Siti Nur Halimah',
|
||||
nim: 'H1031201008',
|
||||
year: 2024,
|
||||
category: 'Kecerdasan Buatan',
|
||||
description: 'Aplikasi mobile berbasis deep learning untuk mendeteksi penyakit tanaman padi secara instan hanya dari foto daun menggunakan kamera smartphone.',
|
||||
image: 'https://picsum.photos/seed/cropai/800/600',
|
||||
link: 'https://github.com/informatika-untan/crop-detector',
|
||||
},
|
||||
{
|
||||
id: 'p5',
|
||||
title: 'Borneo Wildlife Tracker',
|
||||
studentName: 'Ahmad Fauzi',
|
||||
nim: 'H1031211063',
|
||||
year: 2023,
|
||||
category: 'IoT',
|
||||
description: 'Sistem pelacakan satwa liar Kalimantan menggunakan GPS collar dan dashboard web real-time untuk mendukung riset konservasi fauna.',
|
||||
image: 'https://picsum.photos/seed/wildlife/800/600',
|
||||
},
|
||||
{
|
||||
id: 'p6',
|
||||
title: 'Digital UMKM Marketplace',
|
||||
studentName: 'Lestari Dewi Kusuma',
|
||||
nim: 'H1031221028',
|
||||
year: 2024,
|
||||
category: 'Software Engineering',
|
||||
description: 'Platform e-commerce khusus UMKM Kalimantan Barat dengan fitur manajemen stok, laporan keuangan otomatis, dan integrasi pembayaran digital.',
|
||||
image: 'https://picsum.photos/seed/marketplace/800/600',
|
||||
link: 'https://github.com/informatika-untan/umkm-marketplace',
|
||||
},
|
||||
{
|
||||
id: 'p7',
|
||||
title: 'Campus Navigation AR',
|
||||
studentName: 'Bagas Prasetyo',
|
||||
nim: 'H1031231012',
|
||||
year: 2024,
|
||||
category: 'Software Engineering',
|
||||
description: 'Aplikasi augmented reality untuk navigasi kampus UNTAN yang memandu mahasiswa baru menemukan ruang kelas, lab, dan fasilitas secara interaktif.',
|
||||
image: 'https://picsum.photos/seed/ar-nav/800/600',
|
||||
},
|
||||
{
|
||||
id: 'p8',
|
||||
title: 'Flood Early Warning System',
|
||||
studentName: 'Yuni Karlina',
|
||||
nim: 'H1031211088',
|
||||
year: 2023,
|
||||
category: 'IoT',
|
||||
description: 'Sistem peringatan dini banjir berbasis jaringan sensor IoT di sepanjang Sungai Kapuas yang mengirim notifikasi real-time ke warga sekitar.',
|
||||
image: 'https://picsum.photos/seed/flood/800/600',
|
||||
link: 'https://github.com/informatika-untan/flood-warning',
|
||||
},
|
||||
{
|
||||
id: 'p9',
|
||||
title: 'Mental Health Support Chatbot',
|
||||
studentName: 'Rizky Pratama',
|
||||
nim: 'H1031221051',
|
||||
year: 2024,
|
||||
category: 'Kecerdasan Buatan',
|
||||
description: 'Chatbot AI empatik untuk dukungan kesehatan mental mahasiswa, dilengkapi deteksi sentimen dan sistem rujukan ke konselor kampus.',
|
||||
image: 'https://picsum.photos/seed/mentalbot/800/600',
|
||||
},
|
||||
];
|
||||
*/
|
||||
|
||||
export const ACHIEVEMENTS: Achievement[] = [];
|
||||
export const PROJECTS: Project[] = [];
|
||||
|
||||
|
After Width: | Height: | Size: 238 KiB |
@@ -0,0 +1,215 @@
|
||||
export type Lang = 'id' | 'en';
|
||||
|
||||
export const translations = {
|
||||
id: {
|
||||
nav: {
|
||||
tentangKami: 'Tentang Kami',
|
||||
program: 'Program',
|
||||
sdm: 'SDM',
|
||||
fasilitas: 'Fasilitas',
|
||||
karyaAgenda: 'Karya & Agenda',
|
||||
kontak: 'Kontak',
|
||||
satuUntan: 'SATU UNTAN',
|
||||
},
|
||||
hero: {
|
||||
badge: 'MAHASISWA & PRESTASI',
|
||||
title: 'Sorotan dan Prestasi Karya Mahasiswa',
|
||||
desc: 'Galeri pencapaian, penghargaan, dan portofolio inovasi teknologi yang dikembangkan oleh mahasiswa Program Studi Informatika Universitas Tanjungpura.',
|
||||
},
|
||||
about: {
|
||||
title: 'Tentang Program Studi',
|
||||
desc: 'Program Studi Informatika Universitas Tanjungpura adalah program yang berfokus pada pengembangan ilmu komputer, rekayasa perangkat lunak, dan inovasi teknologi informasi. Mahasiswa kami aktif berprestasi di tingkat regional, nasional, maupun internasional, membawa nama baik Kalimantan Barat ke panggung dunia.',
|
||||
tag: 'Program Studi',
|
||||
},
|
||||
stats: {
|
||||
totalPrestasi: 'Total Prestasi',
|
||||
totalProyek: 'Total Proyek Mahasiswa',
|
||||
},
|
||||
achievement: {
|
||||
sectionTitle: 'Prestasi Mahasiswa',
|
||||
sectionDesc: 'Daftar pencapaian dan penghargaan yang diraih oleh mahasiswa Informatika UNTAN di berbagai kompetisi.',
|
||||
filterTitle: 'Filter Prestasi',
|
||||
searchPlaceholder: 'Cari nama, prestasi, bidang...',
|
||||
allLevel: 'Semua Tingkat',
|
||||
allField: 'Semua Bidang',
|
||||
allYear: 'Semua Tahun',
|
||||
resetFilter: 'Reset Filter',
|
||||
noData: 'Tidak ada prestasi yang sesuai filter.',
|
||||
noDataSub: 'Coba ubah pilihan filter atau kata kunci pencarian.',
|
||||
showing: 'Menampilkan',
|
||||
matching: 'prestasi yang sesuai filter',
|
||||
loadMore: 'Muat Lebih Banyak',
|
||||
showLess: 'Tampilkan Lebih Sedikit',
|
||||
allShown: 'prestasi telah ditampilkan.',
|
||||
readMore: 'Baca Selengkapnya',
|
||||
},
|
||||
project: {
|
||||
sectionTitle: 'Top Projects & Portofolio',
|
||||
sectionDesc: 'Jelajahi inovasi digital dan aplikasi nyata yang dikembangkan oleh mahasiswa Informatika UNTAN.',
|
||||
scrollHint: '← Geser untuk melihat lebih banyak →',
|
||||
explore: 'Telusuri Lanjut',
|
||||
visitLink: 'Kunjungi Proyek',
|
||||
noLink: 'Link tidak tersedia',
|
||||
by: 'oleh',
|
||||
readMore: 'Baca Selengkapnya',
|
||||
filterTitle: 'Filter Portofolio',
|
||||
searchPlaceholder: 'Cari judul, mahasiswa, bidang...',
|
||||
allField: 'Semua Bidang',
|
||||
allYear: 'Semua Tahun',
|
||||
resetFilter: 'Reset Filter',
|
||||
noData: 'Tidak ada proyek yang sesuai filter.',
|
||||
noDataSub: 'Coba ubah pilihan filter atau kata kunci pencarian.',
|
||||
showing: 'Menampilkan',
|
||||
matching: 'proyek yang sesuai filter',
|
||||
loadMore: 'Muat Lebih Banyak',
|
||||
showLess: 'Tampilkan Lebih Sedikit',
|
||||
allShown: 'proyek telah ditampilkan.',
|
||||
},
|
||||
modal: {
|
||||
close: 'Tutup',
|
||||
nim: 'NIM',
|
||||
angkatan: 'Angkatan',
|
||||
tahun: 'Tahun',
|
||||
tingkat: 'Tingkat',
|
||||
bidang: 'Bidang',
|
||||
kompetisi: 'Kompetisi',
|
||||
peringkat: 'Peringkat',
|
||||
tim: 'Tim / Nama',
|
||||
deskripsi: 'Deskripsi',
|
||||
mahasiswa: 'Mahasiswa',
|
||||
link: 'Link Proyek',
|
||||
namaProyek: 'Nama Proyek',
|
||||
},
|
||||
cta: {
|
||||
badge: 'HUBUNGI KAMI',
|
||||
title: 'Ingin Tahu Lebih Lanjut?',
|
||||
desc: 'Tertarik bergabung dengan Program Studi Informatika UNTAN, mendaftarkan prestasi mahasiswa, atau menjalin kerja sama? Hubungi kami sekarang.',
|
||||
waButton: 'Chat via WhatsApp',
|
||||
webButton: 'Kunjungi Website Kami',
|
||||
contactTitle: 'Informasi Kontak',
|
||||
emailLabel: 'Email',
|
||||
email: 'informatika@untan.ac.id',
|
||||
phone: '+62 561 577963',
|
||||
address: 'Jl. Prof. Dr. H. Hadari Nawawi, Bansir Laut, Pontianak Tenggara, 78124',
|
||||
},
|
||||
footer: {
|
||||
desc: 'Program Studi Informatika, Universitas Tanjungpura.\nJl. Prof. Dr. H. Hadari Nawawi, Bansir Laut, Kec. Pontianak Tenggara, Kota Pontianak, Kalimantan Barat 78124',
|
||||
kategori: 'Kategori',
|
||||
tautan: 'Tautan Penting',
|
||||
berita: 'Berita & Informasi',
|
||||
pengumuman: 'Pengumuman Akademik',
|
||||
prestasi: 'Prestasi Mahasiswa',
|
||||
siakad: 'SIAKAD UNTAN',
|
||||
elearning: 'E-Learning UNTAN',
|
||||
privacy: 'Kebijakan Privasi',
|
||||
terms: 'Syarat & Ketentuan',
|
||||
rights: 'All rights reserved.',
|
||||
},
|
||||
},
|
||||
|
||||
en: {
|
||||
nav: {
|
||||
tentangKami: 'About Us',
|
||||
program: 'Programs',
|
||||
sdm: 'Human Resources',
|
||||
fasilitas: 'Facilities',
|
||||
karyaAgenda: 'Works & Events',
|
||||
kontak: 'Contact',
|
||||
satuUntan: 'SATU UNTAN',
|
||||
},
|
||||
hero: {
|
||||
badge: 'STUDENTS & ACHIEVEMENTS',
|
||||
title: 'Student Spotlights & Achievements',
|
||||
desc: 'Gallery of accomplishments, awards, and technology innovation portfolios developed by Informatics students of Tanjungpura University.',
|
||||
},
|
||||
about: {
|
||||
title: 'About the Study Program',
|
||||
desc: 'The Informatics Study Program at Tanjungpura University focuses on computer science, software engineering, and information technology innovation. Our students actively excel at regional, national, and international levels, representing West Kalimantan on the world stage.',
|
||||
tag: 'Study Program',
|
||||
},
|
||||
stats: {
|
||||
totalPrestasi: 'Total Achievements',
|
||||
totalProyek: 'Total Student Projects',
|
||||
},
|
||||
achievement: {
|
||||
sectionTitle: 'Student Achievements',
|
||||
sectionDesc: 'List of awards and accomplishments by Informatics UNTAN students in various competitions.',
|
||||
filterTitle: 'Filter Achievements',
|
||||
searchPlaceholder: 'Search name, achievement, field...',
|
||||
allLevel: 'All Levels',
|
||||
allField: 'All Fields',
|
||||
allYear: 'All Years',
|
||||
resetFilter: 'Reset Filter',
|
||||
noData: 'No achievements match the current filter.',
|
||||
noDataSub: 'Try adjusting your filter or search keyword.',
|
||||
showing: 'Showing',
|
||||
matching: 'matching achievements',
|
||||
loadMore: 'Load More',
|
||||
showLess: 'Show Less',
|
||||
allShown: 'achievements displayed.',
|
||||
readMore: 'Read More',
|
||||
},
|
||||
project: {
|
||||
sectionTitle: 'Top Projects & Portfolios',
|
||||
sectionDesc: 'Explore digital innovations and real-world applications developed by Informatics UNTAN students.',
|
||||
scrollHint: '← Swipe to see more →',
|
||||
explore: 'Explore More',
|
||||
visitLink: 'Visit Project',
|
||||
noLink: 'Link unavailable',
|
||||
by: 'by',
|
||||
readMore: 'Read More',
|
||||
filterTitle: 'Filter Portfolios',
|
||||
searchPlaceholder: 'Search title, student, field...',
|
||||
allField: 'All Fields',
|
||||
allYear: 'All Years',
|
||||
resetFilter: 'Reset Filter',
|
||||
noData: 'No projects match the current filter.',
|
||||
noDataSub: 'Try adjusting your filter or search keyword.',
|
||||
showing: 'Showing',
|
||||
matching: 'matching projects',
|
||||
loadMore: 'Load More',
|
||||
showLess: 'Show Less',
|
||||
allShown: 'projects displayed.',
|
||||
},
|
||||
modal: {
|
||||
close: 'Close',
|
||||
nim: 'Student ID',
|
||||
angkatan: 'Batch',
|
||||
tahun: 'Year',
|
||||
tingkat: 'Level',
|
||||
bidang: 'Field',
|
||||
kompetisi: 'Competition',
|
||||
peringkat: 'Rank',
|
||||
tim: 'Team / Name',
|
||||
deskripsi: 'Description',
|
||||
mahasiswa: 'Student',
|
||||
link: 'Project Link',
|
||||
namaProyek: 'Project Name',
|
||||
},
|
||||
cta: {
|
||||
badge: 'CONTACT US',
|
||||
title: 'Want to Know More?',
|
||||
desc: 'Interested in joining the Informatics Study Program at UNTAN, registering a student achievement, or establishing a partnership? Contact us now.',
|
||||
waButton: 'Chat via WhatsApp',
|
||||
webButton: 'Visit Our Website',
|
||||
contactTitle: 'Contact Information',
|
||||
emailLabel: 'Email',
|
||||
address: 'Jl. Prof. Dr. H. Hadari Nawawi, Bansir Laut, Pontianak Tenggara, 78124',
|
||||
},
|
||||
footer: {
|
||||
desc: 'Informatics Study Program, Tanjungpura University.\nJl. Prof. Dr. H. Hadari Nawawi, Bansir Laut, Kec. Pontianak Tenggara, Pontianak City, West Kalimantan 78124',
|
||||
kategori: 'Categories',
|
||||
tautan: 'Important Links',
|
||||
berita: 'News & Information',
|
||||
pengumuman: 'Academic Announcements',
|
||||
prestasi: 'Student Achievements',
|
||||
siakad: 'SIAKAD UNTAN',
|
||||
elearning: 'E-Learning UNTAN',
|
||||
privacy: 'Privacy Policy',
|
||||
terms: 'Terms & Conditions',
|
||||
rights: 'All rights reserved.',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Translations = typeof translations.id;
|
||||
@@ -0,0 +1,69 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@400;700&family=Roboto:wght@300;400;500;700&family=Merriweather:wght@700;900&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Break out of the kit's container to go full-width */
|
||||
.acaab-full-bleed {
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
}
|
||||
|
||||
body { overflow-x: hidden; }
|
||||
|
||||
@theme {
|
||||
--font-sans: "Roboto", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: "Roboto Condensed", sans-serif;
|
||||
--font-serif: "Merriweather", ui-serif, Georgia, serif;
|
||||
|
||||
--color-untan-navy: #003150;
|
||||
--color-untan-yellow: #feb401;
|
||||
--color-untan-blue: #1e40af;
|
||||
--color-primary-navy: #003150;
|
||||
--color-primary-gold: #feb401;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background-color: #ffffff;
|
||||
color: #334155;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
/* Hide scrollbar for carousel while keeping scroll functionality */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Fade-in animation for dynamically loaded cards */
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(16px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.4s ease both;
|
||||
}
|
||||
|
||||
/* Explicitly scan all component files so arbitrary Tailwind classes compile */
|
||||
@source "./**/*.tsx";
|
||||
|
||||
/* Stats bar 3-column layout (replaces md:grid-cols-[1fr_auto_1fr_auto_1fr]) */
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr auto 1fr auto 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Project carousel card widths (replaces w-[85vw] md:w-[60vw] lg:w-[38vw]) */
|
||||
.proj-card { width: 75vw; }
|
||||
@media (min-width: 768px) { .proj-card { width: 42vw; } }
|
||||
@media (min-width: 1024px) { .proj-card { width: 28vw; } }
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ACAAB — Dev Preview</title>
|
||||
<style>
|
||||
#dev-banner {
|
||||
background: #feb401; color: #003150; font-size: 11px; font-weight: 700;
|
||||
text-align: center; padding: 6px; letter-spacing: .05em; position: sticky; top: 0; z-index: 999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="dev-banner">⚡ DEV MODE — Navbar & footer tidak tampil di sini. Gunakan kit dev server (root) untuk preview lengkap.</div>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Achievement, Project } from '../data';
|
||||
|
||||
// Determine API Base URL dynamically from Vite env, fallback to hardcoded if not set
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.ifuntanhub.dev';
|
||||
|
||||
/**
|
||||
* Helper to parse Rank/Juara from Achievement title
|
||||
*/
|
||||
function extractRank(title: string): string {
|
||||
const match = title.match(/(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)/i);
|
||||
return match ? match[0] : 'Pemenang';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to parse Level (Nasional, Internasional, Regional) from title/category
|
||||
*/
|
||||
function extractLevel(title: string, category: string): 'Nasional' | 'Internasional' | 'Regional' {
|
||||
const text = `${title} ${category}`.toLowerCase();
|
||||
if (
|
||||
text.includes('internasional') ||
|
||||
text.includes('international') ||
|
||||
text.includes('global') ||
|
||||
text.includes('world') ||
|
||||
text.includes('asean') ||
|
||||
text.includes('unesco')
|
||||
) {
|
||||
return 'Internasional';
|
||||
}
|
||||
if (
|
||||
text.includes('regional') ||
|
||||
text.includes('kalbar') ||
|
||||
text.includes('kalimantan') ||
|
||||
text.includes('provinsi') ||
|
||||
text.includes('kota') ||
|
||||
text.includes('kabupaten') ||
|
||||
text.includes('lokal')
|
||||
) {
|
||||
return 'Regional';
|
||||
}
|
||||
return 'Nasional'; // Default fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to extract competition name from full achievement title
|
||||
*/
|
||||
function extractCompetition(title: string): string {
|
||||
return title
|
||||
.replace(/^(Juara\s+\d+|Juara\s+Harapan\s*\d*|Top\s+\d+|Finalis|Peringkat\s+\d+|Medali\s+\w+)\s+(di\s+|pada\s+|dalam\s+)?/i, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch achievements from API
|
||||
*/
|
||||
export async function fetchAchievementsFromAPI(): Promise<Achievement[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/items/prestasi`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch achievements: ${response.statusText}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
const data = result.data || [];
|
||||
|
||||
return data
|
||||
.filter((item: any) => item.status === 'published')
|
||||
.map((item: any) => {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_prestasi || 'Untitled Achievement';
|
||||
const category = item.bidang || 'Akademik';
|
||||
const rank = extractRank(title);
|
||||
const level = extractLevel(title, category);
|
||||
const competition = extractCompetition(title);
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const team = item.nama_tim || 'Mahasiswa';
|
||||
|
||||
// Handle image url mapping for directus assets
|
||||
const image = item.foto
|
||||
? `${API_BASE_URL}/assets/${item.foto}`
|
||||
: `https://picsum.photos/seed/prestasi-${id}/800/600`;
|
||||
|
||||
const avatar = (item.nama_tim || item.nama_prestasi || 'IF')
|
||||
.substring(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
// Create a friendly fallback description since Directus model lacks one
|
||||
const description = `${team} berhasil meraih prestasi luar biasa sebagai ${rank} dalam ajang ${competition} tahun ${year} pada bidang ${category.toLowerCase()}.`;
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
category,
|
||||
competition,
|
||||
rank,
|
||||
year,
|
||||
level,
|
||||
description,
|
||||
team,
|
||||
nim: item.nim || undefined,
|
||||
generation: item.angkatan ? `Angkatan ${item.angkatan}` : 'Informatika',
|
||||
image,
|
||||
avatar,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch projects/portfolio from API
|
||||
*/
|
||||
export async function fetchProjectsFromAPI(): Promise<Project[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/items/portfolio`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch portfolio: ${response.statusText}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
const data = result.data || [];
|
||||
|
||||
return data
|
||||
.filter((item: any) => item.status === 'published')
|
||||
.map((item: any) => {
|
||||
const id = String(item.id);
|
||||
const title = item.nama_proyek || 'Untitled Project';
|
||||
const studentName = item.nama_mahasiswa || 'Mahasiswa';
|
||||
const year = parseInt(item.tahun) || new Date().getFullYear();
|
||||
const category = item.bidang || 'Software Development';
|
||||
const description = item.deskripsi || 'Tidak ada deskripsi.';
|
||||
let link = item.url || undefined;
|
||||
if (link && !/^https?:\/\//i.test(link)) {
|
||||
link = `https://${link}`;
|
||||
}
|
||||
|
||||
const image = item.foto
|
||||
? `${API_BASE_URL}/assets/${item.foto}`
|
||||
: `https://picsum.photos/seed/project-${id}/800/600`;
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
studentName,
|
||||
nim: item.nim || undefined,
|
||||
year,
|
||||
category,
|
||||
description,
|
||||
image,
|
||||
link,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Achievement, Project } from '../data';
|
||||
|
||||
/**
|
||||
* Google Sheets Service
|
||||
*/
|
||||
|
||||
const SPREADSHEET_ID = import.meta.env.VITE_SPREADSHEET_ID;
|
||||
const API_KEY = import.meta.env.VITE_GOOGLE_API_KEY;
|
||||
|
||||
export async function fetchSheetData(range: string) {
|
||||
const url = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/${range}?key=${API_KEY}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.values;
|
||||
} catch (error) {
|
||||
console.error('Error fetching sheet data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMappedData() {
|
||||
const values = await fetchSheetData('Form Responses 1!A2:J200');
|
||||
if (!values) return { achievements: [], projects: [] };
|
||||
|
||||
const achievements: Achievement[] = [];
|
||||
const projects: Project[] = [];
|
||||
|
||||
values.forEach((row: any[], index: number) => {
|
||||
// New Mapping:
|
||||
// 0: Nama Mahasiswa (A)
|
||||
// 1: NIM Mahasiswa (B)
|
||||
// 2: Angkatan (C)
|
||||
// 3: Jenis Data (D)
|
||||
// 4: Nama Prestasi (E)
|
||||
// 5: Tahun (F)
|
||||
// 6: Tingkat (G)
|
||||
// 7: Bidang (H)
|
||||
// 8: Deskripsi (I)
|
||||
// 9: Gambar (J)
|
||||
|
||||
const type = row[3]; // Jenis Data
|
||||
const id = `sheet-${index}`;
|
||||
const imageUrl = row[9] || `https://picsum.photos/seed/${id}/800/600`;
|
||||
|
||||
if (type === 'Prestasi') {
|
||||
achievements.push({
|
||||
id,
|
||||
title: row[4] || 'Untitled Achievement',
|
||||
category: row[7] || 'Umum',
|
||||
competition: row[4] || 'Kompetisi',
|
||||
rank: 'Peserta',
|
||||
year: parseInt(row[5]) || new Date().getFullYear(),
|
||||
level: (row[6] as any) || 'Nasional',
|
||||
description: row[8] || '',
|
||||
team: row[0] || 'Mahasiswa',
|
||||
nim: row[1],
|
||||
generation: `Angkatan ${row[2] || '-'}`,
|
||||
image: imageUrl,
|
||||
avatar: (row[0] || 'M').substring(0, 2).toUpperCase(),
|
||||
});
|
||||
} else if (type === 'Proyek') {
|
||||
projects.push({
|
||||
id: `p-${id}`,
|
||||
title: row[4] || 'Untitled Project',
|
||||
studentName: row[0] || 'Mahasiswa',
|
||||
nim: row[1],
|
||||
year: parseInt(row[5]) || new Date().getFullYear(),
|
||||
category: row[7] || 'Software',
|
||||
description: row[8] || '',
|
||||
image: imageUrl,
|
||||
link: undefined, // No link column mentioned in new structure
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { achievements, projects };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SPREADSHEET_ID: string
|
||||
readonly VITE_GOOGLE_API_KEY: string
|
||||
readonly VITE_API_BASE_URL: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/* =====================================================
|
||||
ACAAB – Prestasi & Karya Mahasiswa
|
||||
Custom styles yang tidak dicakup output.css
|
||||
===================================================== */
|
||||
|
||||
/* Cegah horizontal scroll tanpa clip full-bleed */
|
||||
body { overflow-x: hidden; }
|
||||
|
||||
/* --- Full-bleed: section keluar dari container prose --- */
|
||||
.acaab-full-bleed {
|
||||
width: 100vw;
|
||||
margin-left: calc(50% - 50vw);
|
||||
margin-right: calc(50% - 50vw);
|
||||
}
|
||||
|
||||
/* --- Sembunyikan scrollbar carousel tapi tetap scrollable --- */
|
||||
.acaab-hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.acaab-hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Card ukuran responsif di carousel --- */
|
||||
.acaab-proj-card {
|
||||
width: 80vw;
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
@media (min-width: 768px) { .acaab-proj-card { width: 44vw; } }
|
||||
@media (min-width: 1024px) { .acaab-proj-card { width: 30vw; } }
|
||||
|
||||
/* --- Snap scroll carousel --- */
|
||||
.acaab-carousel-track {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* --- Animasi fade-in untuk cards --- */
|
||||
@keyframes acaab-fade-in {
|
||||
from { opacity: 0; transform: translateY(14px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.acaab-fade-in {
|
||||
animation: acaab-fade-in 0.35s ease both;
|
||||
}
|
||||
|
||||
/* --- Badge level warna --- */
|
||||
.acaab-badge-intl { background-color: #ca8a04; color: #fff; }
|
||||
.acaab-badge-nasional { background-color: #1d4ed8; color: #fff; }
|
||||
.acaab-badge-regional { background-color: #64748b; color: #fff; }
|
||||
|
||||
/* --- Skeleton loading --- */
|
||||
.acaab-skeleton {
|
||||
background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: acaab-skeleton-pulse 1.5s infinite;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@keyframes acaab-skeleton-pulse {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* --- Modal backdrop --- */
|
||||
.acaab-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* --- Scroll lock saat modal terbuka --- */
|
||||
body.acaab-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Stats grid: divider vertikal di desktop --- */
|
||||
@media (min-width: 768px) {
|
||||
.acaab-stats-grid {
|
||||
grid-template-columns: 1fr 1px 1fr 1px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Line clamp (browser lama) --- */
|
||||
.acaab-line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.acaab-line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* --- Achievement card --- */
|
||||
.acaab-ach-card {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.acaab-ach-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.12), 0 4px 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
.acaab-ach-top-bar {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
background: #003150;
|
||||
z-index: 10;
|
||||
transition: background 0.25s ease;
|
||||
}
|
||||
.acaab-ach-card:hover .acaab-ach-top-bar { background: #feb401; }
|
||||
.acaab-ach-card:hover .acaab-card-title { color: #1e40af; }
|
||||
.acaab-card-title { transition: color 0.2s ease; }
|
||||
.acaab-ach-read-btn:hover { background: #003150 !important; color: white !important; }
|
||||
|
||||
/* --- Project card wrapper --- */
|
||||
.acaab-proj-card-wrap {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||
border: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.acaab-proj-card-wrap:hover { box-shadow: 0 10px 25px rgba(0,0,0,0.12); }
|
||||
.acaab-proj-left-bar {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 3px; height: 100%;
|
||||
background: #feb401;
|
||||
z-index: 10;
|
||||
transform-origin: bottom;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.acaab-proj-card-wrap:hover .acaab-proj-left-bar { transform: scaleY(1.08); }
|
||||
.acaab-proj-card-wrap:hover .acaab-card-img { transform: scale(1.05); }
|
||||
|
||||
/* --- Card image zoom --- */
|
||||
.acaab-card-img {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
/* --- Stats grid dividers responsive --- */
|
||||
.acaab-stat-vdivider { display: none; }
|
||||
@media (min-width: 768px) {
|
||||
.acaab-stat-vdivider { display: flex; }
|
||||
.acaab-stat-hdivider { display: none; }
|
||||
}
|
||||
|
||||
/* --- Filter focus --- */
|
||||
#acaab-ach-filters input:focus,
|
||||
#acaab-ach-filters select:focus,
|
||||
#acaab-proj-filters input:focus,
|
||||
#acaab-proj-filters select:focus {
|
||||
outline: 2px solid #003150;
|
||||
outline-offset: 1px;
|
||||
border-color: #003150;
|
||||
}
|
||||
|
||||
/* --- CTA inner layout: stacked mobile → row desktop --- */
|
||||
.acaab-cta-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.acaab-cta-inner {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
text-align: left;
|
||||
gap: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Reduced motion --- */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
}
|
||||
|
||||
/* --- Modal Responsiveness Overrides --- */
|
||||
.acaab-modal-achievement {
|
||||
max-width: 500px !important;
|
||||
}
|
||||
|
||||
.acaab-modal-project {
|
||||
max-width: 700px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.acaab-modal-achievement, .acaab-modal-project {
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 1rem 1rem 0 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(({ command }) => ({
|
||||
plugins: [react(), tailwindcss()],
|
||||
|
||||
// Relative base so asset URLs resolve correctly from any subdirectory
|
||||
base: './',
|
||||
|
||||
// Dev: root = src/ so HMR works against src/index.html
|
||||
// Build: root = . so rollup can resolve src/index.html as input
|
||||
root: command === 'serve' ? './src' : '.',
|
||||
|
||||
resolve: {
|
||||
alias: { '@': path.resolve(__dirname, '.') },
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true,
|
||||
},
|
||||
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, 'assets'),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: path.resolve(__dirname, 'src/index.html'),
|
||||
output: {
|
||||
entryFileNames: 'acaab.js',
|
||||
chunkFileNames: 'acaab-[name].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
const name = assetInfo.names?.[0] ?? '';
|
||||
if (name.endsWith('.css')) return 'acaab.css';
|
||||
return 'acaab-[name][extname]';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
Before Width: | Height: | Size: 7.4 MiB |
|
Before Width: | Height: | Size: 848 KiB |
|
Before Width: | Height: | Size: 755 KiB |
@@ -1,82 +0,0 @@
|
||||
// =====================================================
|
||||
// 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 (API remote maupun lokal) 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: item.thumbnail || '',
|
||||
deskripsi_singkat: item.deskripsi_singkat || parts.join(' · ') || 'Info beasiswa tersedia di sumber resmi.',
|
||||
deskripsi_singkat_en: item.deskripsi_singkat_en || '',
|
||||
tanggal_buka: item.tanggal_buka || '',
|
||||
tanggal_tutup: item.tenggat_waktu || '',
|
||||
url_sumber: item.url || '',
|
||||
syarat_umum: item.syarat_umum || [],
|
||||
syarat_umum_en: item.syarat_umum_en || [],
|
||||
kontak: item.kontak || '',
|
||||
tags: item.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();
|
||||
const raw = Array.isArray(json) ? json : json.data || [];
|
||||
return raw.map(normalizeItem);
|
||||
}
|
||||
|
||||
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);
|
||||
try {
|
||||
const data = await fetchLocal();
|
||||
return { data, source: 'local' };
|
||||
} catch (localErr) {
|
||||
console.error('[BeasiswaAPI] Semua sumber gagal:', localErr.message);
|
||||
throw localErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.BeasiswaAPI = { config: CONFIG, getBeasiswa, fetchRemote, fetchLocal };
|
||||
})(window);
|
||||
@@ -1,140 +0,0 @@
|
||||
// =====================================================
|
||||
// carousel.js — Swiper.js Carousel Testimoni
|
||||
// Touch-friendly, auto-play, responsive breakpoints
|
||||
// + DUKUNGAN MULTI-BAHASA (ID/EN) Terintegrasi
|
||||
// =====================================================
|
||||
|
||||
let testimoniSwiper = null;
|
||||
let allTestimoniData = []; // Simpan data di memori agar tidak perlu loading ulang saat ganti bahasa
|
||||
|
||||
// 1. Variabel penampung bahasa yang terhubung otomatis dengan tombol ID/EN
|
||||
let activeCarouselLang = localStorage.getItem('if_untan_lang') || document.documentElement.lang || 'id';
|
||||
|
||||
// 2. Tangkap event perubahan bahasa secara langsung dari localization.js
|
||||
document.addEventListener('languageChanged', (e) => {
|
||||
// Update bahasa
|
||||
if (e.detail && e.detail.lang) {
|
||||
activeCarouselLang = e.detail.lang;
|
||||
} else if (typeof window.getLang === 'function') {
|
||||
activeCarouselLang = window.getLang();
|
||||
} else {
|
||||
activeCarouselLang = document.documentElement.lang || 'id';
|
||||
}
|
||||
|
||||
// Jika data sudah ada, langsung cetak ulang testimoninya
|
||||
if (allTestimoniData && allTestimoniData.length > 0) {
|
||||
renderTestimoniSlides(allTestimoniData);
|
||||
initSwiper(allTestimoniData.length); // Swiper harus di-restart agar animasinya tidak rusak
|
||||
}
|
||||
});
|
||||
|
||||
/** Render slide testimoni ke dalam swiper-wrapper */
|
||||
function renderTestimoniSlides(data) {
|
||||
const wrapper = document.getElementById('testimonial-wrapper');
|
||||
if (!wrapper) return;
|
||||
|
||||
const lang = activeCarouselLang; // Gunakan bahasa terbaru
|
||||
|
||||
wrapper.innerHTML = data.map(t => {
|
||||
// Logika Pintar: Cek mode bahasa. Jika 'en' dan teks terjemahannya ada, pakai bahasa Inggris.
|
||||
// Jika tidak, tetap tampilkan teks bahasa Indonesia (sebagai cadangan).
|
||||
const beasiswaName = (lang === 'en' && t.nama_beasiswa_en) ? t.nama_beasiswa_en : t.nama_beasiswa;
|
||||
const quote = (lang === 'en' && t.kutipan_en) ? t.kutipan_en : t.kutipan;
|
||||
|
||||
return `
|
||||
<div class="swiper-slide" style="height:auto;">
|
||||
<div class="testimonial-card">
|
||||
<img
|
||||
src="${t.foto}"
|
||||
alt="Foto ${t.nama}"
|
||||
class="testimonial-avatar"
|
||||
loading="lazy"
|
||||
onerror="
|
||||
this.style.display='none';
|
||||
this.nextElementSibling.style.display='flex';
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="testimonial-avatar"
|
||||
style="
|
||||
display:none;
|
||||
background:linear-gradient(135deg,#003150,#4a6bc5);
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
color:white;
|
||||
font-size:22px;
|
||||
font-weight:700;
|
||||
"
|
||||
>
|
||||
${t.nama.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div class="testimonial-name">${t.nama}</div>
|
||||
<div class="testimonial-beasiswa">${beasiswaName}</div>
|
||||
<p class="testimonial-quote">"${quote}"</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/** Inisialisasi Swiper setelah slide di-render */
|
||||
function initSwiper(count) {
|
||||
// Bersihkan swiper lama jika ada (penting agar tidak menumpuk saat ganti bahasa)
|
||||
if (testimoniSwiper) {
|
||||
testimoniSwiper.destroy(true, true);
|
||||
testimoniSwiper = null;
|
||||
}
|
||||
|
||||
testimoniSwiper = new Swiper('.testimonial-swiper', {
|
||||
slidesPerView: 1,
|
||||
spaceBetween: 24,
|
||||
loop: count > 3,
|
||||
centerInsufficientSlides: true,
|
||||
autoplay: {
|
||||
delay: 4500,
|
||||
disableOnInteraction: false,
|
||||
pauseOnMouseEnter: true,
|
||||
},
|
||||
pagination: {
|
||||
el: '.testimonial-pagination',
|
||||
clickable: true,
|
||||
},
|
||||
grabCursor: true,
|
||||
breakpoints: {
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 3 },
|
||||
},
|
||||
// Dukungan Aksesibilitas Dinamis (VoiceOver / Screen Reader)
|
||||
a11y: {
|
||||
prevSlideMessage: activeCarouselLang === 'en' ? 'Previous slide' : 'Slide sebelumnya',
|
||||
nextSlideMessage: activeCarouselLang === 'en' ? 'Next slide' : 'Slide berikutnya',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch data testimoni dan render */
|
||||
async function initCarousel() {
|
||||
const section = document.getElementById('testimonial');
|
||||
|
||||
try {
|
||||
const res = await fetch('data/testimoni.json');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
allTestimoniData = data; // Simpan ke variabel global
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
section?.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
renderTestimoniSlides(data);
|
||||
requestAnimationFrame(() => initSwiper(data.length));
|
||||
|
||||
} catch {
|
||||
section?.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initCarousel);
|
||||
@@ -1,53 +0,0 @@
|
||||
// =====================================================
|
||||
// counter.js — Animasi Counter Statistik
|
||||
// Menggunakan IntersectionObserver + requestAnimationFrame
|
||||
// =====================================================
|
||||
|
||||
/**
|
||||
* Animasi angka dari 0 ke nilai target
|
||||
* @param {HTMLElement} el - Elemen yang menampilkan angka
|
||||
* @param {number} target - Angka target
|
||||
* @param {number} duration - Durasi animasi dalam ms
|
||||
*/
|
||||
function animateCounter(el, target, duration = 1800) {
|
||||
const startTime = performance.now();
|
||||
|
||||
function update(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Ease out cubic — percepat di awal, lambat di akhir
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.round(eased * target);
|
||||
|
||||
el.textContent = current;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
function initCounters() {
|
||||
const counters = document.querySelectorAll('[data-counter]');
|
||||
if (!counters.length) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && !entry.target.dataset.animated) {
|
||||
entry.target.dataset.animated = 'true';
|
||||
const target = parseInt(entry.target.dataset.counter, 10) || 0;
|
||||
animateCounter(entry.target, target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.6 }
|
||||
);
|
||||
|
||||
counters.forEach(el => observer.observe(el));
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initCounters);
|
||||
@@ -1,288 +0,0 @@
|
||||
// =====================================================
|
||||
// filter.js — Fetch JSON, Render Kartu, Filter Tab, Pencarian, Paginasi
|
||||
// Filter dan search berjalan KUMULATIF
|
||||
// + DUKUNGAN MULTI-BAHASA (ID/EN) Terintegrasi
|
||||
// =====================================================
|
||||
|
||||
const ITEMS_PER_PAGE = 6;
|
||||
|
||||
let allData = [];
|
||||
let activeFilter = 'semua';
|
||||
let searchKeyword = '';
|
||||
let currentPage = 1;
|
||||
|
||||
// 1. Variabel penampung bahasa yang terhubung otomatis dengan tombol ID/EN
|
||||
let activeLang = localStorage.getItem('if_untan_lang') || document.documentElement.lang || 'id';
|
||||
|
||||
// 2. Tangkap event perubahan bahasa secara langsung dari localization.js
|
||||
document.addEventListener('languageChanged', (e) => {
|
||||
if (e.detail && e.detail.lang) {
|
||||
activeLang = e.detail.lang; // Perbarui bahasa!
|
||||
} else {
|
||||
activeLang = document.documentElement.lang || 'id';
|
||||
}
|
||||
|
||||
// Jika data sudah ada, langsung cetak ulang kartunya
|
||||
if (allData.length > 0) {
|
||||
renderCards();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Format tanggal cerdas berdasarkan bahasa
|
||||
function formatDate(str) {
|
||||
if (!str) return '—';
|
||||
const d = new Date(str);
|
||||
if (isNaN(d.getTime())) return str;
|
||||
|
||||
return d.toLocaleDateString(activeLang === 'en' ? 'en-US' : 'id-ID', {
|
||||
day: 'numeric', month: 'long', year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Kamus mini untuk teks dinamis dari JSON (Negara & Kategori)
|
||||
function translateDynamic(text, lang) {
|
||||
if (!text || lang === 'id') return text;
|
||||
|
||||
const dict = {
|
||||
'Amerika Serikat': 'United States',
|
||||
'Swiss': 'Switzerland',
|
||||
'Pemerintah': 'Government',
|
||||
'Swasta': 'Private',
|
||||
};
|
||||
|
||||
return dict[text] || text;
|
||||
}
|
||||
|
||||
function sanitize(str) {
|
||||
return str.replace(/[<>&"']/g, c =>
|
||||
({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c])
|
||||
);
|
||||
}
|
||||
|
||||
function createCard(bsw) {
|
||||
const lang = activeLang; // Selalu gunakan bahasa aktif terbaru
|
||||
|
||||
// Terjemahan Tombol Status
|
||||
let statusClass = 'status-btn-ditutup';
|
||||
let statusLabel = lang === 'en' ? 'Closed' : 'Ditutup';
|
||||
|
||||
const statusLower = (bsw.status || '').toLowerCase();
|
||||
if (statusLower === 'buka' || statusLower === 'dibuka') {
|
||||
statusClass = 'status-btn-buka';
|
||||
statusLabel = lang === 'en' ? 'Registration Open' : 'Pendaftaran Dibuka';
|
||||
} else if (statusLower.includes('segera')) {
|
||||
statusClass = 'status-btn-segera';
|
||||
statusLabel = lang === 'en' ? 'Opening Soon' : 'Segera Dibuka';
|
||||
}
|
||||
|
||||
// --- KAMUS MINI UNTUK KARTU ---
|
||||
const textMulai = lang === 'en' ? 'Start' : 'Mulai';
|
||||
const textDeadline = lang === 'en' ? 'Deadline' : 'Tenggat';
|
||||
const textSelengkapnya = lang === 'en' ? 'Read More' : 'Selengkapnya';
|
||||
|
||||
// Ambil pills (tags) dan terjemahkan negaranya
|
||||
const tags = Array.isArray(bsw.tags) ? bsw.tags : [];
|
||||
const jenjang = tags[0] || '';
|
||||
const skipPatterns = /^(S1|S2|S3|D3|D4|Swasta|Pemerintah|Fully Funded|Partially Funded|Beasiswa Penuh|Beasiswa Sebagian|Prestasi)$/i;
|
||||
const rawNegara = tags.slice(1).find(t => !skipPatterns.test(t)) || '';
|
||||
const negara = translateDynamic(rawNegara, lang);
|
||||
|
||||
// Terjemahan Subtitle (Kategori Beasiswa)
|
||||
const tipe = bsw.kategori || '';
|
||||
const rawTipe = tipe.replace(/^beasiswa\s+/i, '');
|
||||
let subtitle = '';
|
||||
if (rawTipe) {
|
||||
if (lang === 'en') {
|
||||
const tipeEn = translateDynamic(rawTipe, lang);
|
||||
subtitle = `${tipeEn} Scholarship`;
|
||||
} else {
|
||||
subtitle = `Beasiswa ${rawTipe}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil nama dari versi Inggris jika ada
|
||||
const namaBsw = (lang === 'en' && bsw.nama_en) ? bsw.nama_en : bsw.nama;
|
||||
|
||||
const pillsHtml = [jenjang, negara]
|
||||
.filter(Boolean)
|
||||
.map((t, i) => `<span class="card-pill ${i === 0 ? 'card-pill-level' : ''}">${t}</span>`)
|
||||
.join('');
|
||||
|
||||
const article = document.createElement('article');
|
||||
article.className = 'scholarship-card fade-in-up';
|
||||
article.setAttribute('role', 'listitem');
|
||||
article.setAttribute('data-id', bsw.id);
|
||||
|
||||
article.innerHTML = `
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">${namaBsw}</h3>
|
||||
${subtitle ? `<p class="card-subtitle">${subtitle}</p>` : ''}
|
||||
${pillsHtml ? `<div class="card-pills">${pillsHtml}</div>` : ''}
|
||||
<hr class="card-divider">
|
||||
<div class="card-dates">
|
||||
<div class="card-date-col">
|
||||
<span class="card-date-label">${textMulai}</span>
|
||||
<span class="card-date-value">${formatDate(bsw.tanggal_buka)}</span>
|
||||
</div>
|
||||
<div class="card-date-col">
|
||||
<span class="card-date-label">${textDeadline}</span>
|
||||
<span class="card-date-value">${formatDate(bsw.tanggal_tutup)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="card-status-btn ${statusClass}" disabled>${statusLabel}</button>
|
||||
<button
|
||||
class="btn-selengkapnya"
|
||||
onclick="openPopup('${bsw.id}')"
|
||||
aria-label="Lihat detail beasiswa ${namaBsw}"
|
||||
>${textSelengkapnya}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
function getFiltered() {
|
||||
const kw = searchKeyword.toLowerCase();
|
||||
return allData.filter(bsw => {
|
||||
|
||||
// Logika Filter Tab S1 & Studi Lanjut
|
||||
let matchFilter = false;
|
||||
const tags = bsw.tags || [];
|
||||
|
||||
if (activeFilter === 'semua') {
|
||||
matchFilter = true;
|
||||
} else if (activeFilter === 's1') {
|
||||
matchFilter = tags.some(t => t.includes('S1'));
|
||||
} else if (activeFilter === 'studi_lanjut') {
|
||||
matchFilter = tags.some(t => t.includes('S2') || t.includes('S3'));
|
||||
}
|
||||
|
||||
// Logika Pencarian Search Bar
|
||||
const matchSearch = !kw ||
|
||||
bsw.nama.toLowerCase().includes(kw) ||
|
||||
bsw.deskripsi_singkat.toLowerCase().includes(kw) ||
|
||||
tags.some(t => t.toLowerCase().includes(kw));
|
||||
|
||||
return matchFilter && matchSearch;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCards() {
|
||||
const grid = document.getElementById('beasiswa-grid');
|
||||
const pagCont = document.getElementById('pagination');
|
||||
const filtered = getFiltered();
|
||||
const lang = activeLang; // Gunakan bahasa terbaru
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
const emptyTitle = lang === 'en' ? 'No scholarships found' : 'Tidak ada beasiswa ditemukan';
|
||||
const emptyDesc = lang === 'en' ? 'Try adjusting your filters or search keywords' : 'Coba ubah filter atau kata kunci pencarian Anda';
|
||||
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<i class="fas fa-search-minus"></i>
|
||||
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">${emptyTitle}</p>
|
||||
<p style="font-size:13px;color:#cbd5e1;">${emptyDesc}</p>
|
||||
</div>`;
|
||||
pagCont.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(filtered.length / ITEMS_PER_PAGE);
|
||||
if (currentPage > totalPages) currentPage = totalPages;
|
||||
const start = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
const items = filtered.slice(start, start + ITEMS_PER_PAGE);
|
||||
|
||||
items.forEach((bsw, i) => {
|
||||
const card = createCard(bsw);
|
||||
card.style.animationDelay = `${i * 55}ms`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
renderPagination(totalPages, pagCont);
|
||||
}
|
||||
|
||||
function renderPagination(total, container) {
|
||||
container.innerHTML = '';
|
||||
if (total <= 1) return;
|
||||
|
||||
const make = (content, page, disabled = false, isActive = false) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `page-btn${isActive ? ' active' : ''}`;
|
||||
btn.innerHTML = content;
|
||||
btn.disabled = disabled;
|
||||
if (!disabled) {
|
||||
btn.addEventListener('click', () => {
|
||||
currentPage = page;
|
||||
renderCards();
|
||||
const dir = document.getElementById('directory');
|
||||
if (dir) {
|
||||
const top = dir.getBoundingClientRect().top + window.scrollY - 120;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
return btn;
|
||||
};
|
||||
|
||||
container.appendChild(make('<i class="fas fa-chevron-left" style="font-size:11px;"></i>', currentPage - 1, currentPage === 1));
|
||||
for (let i = 1; i <= total; i++) {
|
||||
container.appendChild(make(i, i, false, i === currentPage));
|
||||
}
|
||||
container.appendChild(make('<i class="fas fa-chevron-right" style="font-size:11px;"></i>', currentPage + 1, currentPage === total));
|
||||
}
|
||||
|
||||
function initFilterTabs() {
|
||||
document.querySelectorAll('.filter-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-tab').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
activeFilter = btn.dataset.filter;
|
||||
currentPage = 1;
|
||||
renderCards();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSearch() {
|
||||
const input = document.getElementById('search-input');
|
||||
if (!input) return;
|
||||
input.addEventListener('input', () => {
|
||||
searchKeyword = sanitize(input.value).trim();
|
||||
currentPage = 1;
|
||||
renderCards();
|
||||
});
|
||||
}
|
||||
|
||||
async function initDirectory() {
|
||||
const grid = document.getElementById('beasiswa-grid');
|
||||
try {
|
||||
if (!window.BeasiswaAPI) throw new Error('Modul API.js (BeasiswaAPI) belum dimuat');
|
||||
const { data, source } = await window.BeasiswaAPI.getBeasiswa();
|
||||
allData = data;
|
||||
window.beasiswaData = allData;
|
||||
renderCards();
|
||||
} catch (err) {
|
||||
const lang = activeLang;
|
||||
const errTitle = lang === 'en' ? 'Failed to load scholarship data' : 'Gagal memuat data beasiswa';
|
||||
const errDesc = lang === 'en' ? 'Could not connect to the server. Please refresh or contact admin.' : 'Tidak dapat terhubung ke server. Silakan refresh atau hubungi admin.';
|
||||
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<i class="fas fa-exclamation-triangle" style="color:#fca5a5;"></i>
|
||||
<p style="font-size:15px;font-weight:700;color:#94a3b8;margin-bottom:6px;">${errTitle}</p>
|
||||
<p style="font-size:13px;color:#cbd5e1;">${errDesc}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initFilterTabs();
|
||||
initSearch();
|
||||
initDirectory();
|
||||
});
|
||||
@@ -1,173 +0,0 @@
|
||||
// =====================================================
|
||||
// main.js — Inisialisasi Utama
|
||||
// Sticky Navbar, Hamburger Menu, Smooth Scroll, AOS, Active Nav
|
||||
// =====================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// ─── AOS (Animate on Scroll) ────────────────────────
|
||||
AOS.init({
|
||||
duration: 700,
|
||||
easing: 'ease-out-cubic',
|
||||
once: true,
|
||||
offset: 50,
|
||||
});
|
||||
|
||||
|
||||
// ─── Sticky Navbar ──────────────────────────────────
|
||||
// Navbar sudah fixed di CSS; tidak perlu mengubah warna karena
|
||||
// warna primary sudah menjadi identitas halaman ini.
|
||||
// Namun kita tambahkan shadow saat scroll untuk depth effect.
|
||||
|
||||
// PERBAIKAN: Menyesuaikan ID dengan yang ada di index.html (nav-wrapper)
|
||||
const navbar = document.getElementById('nav-wrapper') || document.getElementById('navbar');
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!navbar) return; // PERBAIKAN: Mencegah error jika elemen tidak ada di halaman
|
||||
|
||||
if (window.scrollY > 10) {
|
||||
navbar.style.boxShadow = '0 4px 20px rgba(0,0,0,0.25)';
|
||||
} else {
|
||||
navbar.style.boxShadow = 'none';
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
handleScroll(); // initial call
|
||||
|
||||
|
||||
// ─── Ticker Visibility (Muncul setelah Hero & Stats) ─
|
||||
const ticker = document.querySelector('.ticker-wrapper');
|
||||
const statsSection = document.getElementById('statistics');
|
||||
|
||||
const handleTickerVisibility = () => {
|
||||
if (!ticker || !statsSection) return;
|
||||
|
||||
// Ticker muncul setelah melewati batas bawah elemen statistik
|
||||
const statsBottom = statsSection.offsetTop + statsSection.offsetHeight;
|
||||
|
||||
if (window.scrollY > statsBottom) {
|
||||
ticker.classList.add('show-ticker');
|
||||
} else {
|
||||
ticker.classList.remove('show-ticker');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleTickerVisibility, { passive: true });
|
||||
// Call once on load with a slight delay to ensure layout is ready
|
||||
setTimeout(handleTickerVisibility, 100);
|
||||
|
||||
|
||||
// ─── Hamburger Menu (Mobile) ─────────────────────────
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn = document.getElementById('close-mobile-menu');
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
mobileMenu?.classList.add('-translate-x-full');
|
||||
mobileBtn?.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
|
||||
// openMobileMenu handled by inline script in HTML; this covers close-on-scroll
|
||||
closeMobileBtn?.addEventListener('click', closeMobileMenu);
|
||||
|
||||
|
||||
// ─── Smooth Scroll untuk anchor links ──────────────
|
||||
// Sesuai SDD: CSS scroll-behavior + JS fallback dengan offset
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', e => {
|
||||
const href = anchor.getAttribute('href');
|
||||
const target = document.querySelector(href);
|
||||
if (!target) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
// PERBAIKAN: Mencegah error saat membaca offsetHeight jika navbar null
|
||||
const navHeight = navbar ? navbar.offsetHeight : 110;
|
||||
const top = target.getBoundingClientRect().top + window.scrollY - navHeight - 4;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
|
||||
// Tutup mobile menu jika sedang terbuka
|
||||
closeMobileMenu();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// ─── Active Nav Link via IntersectionObserver ──────
|
||||
// Sesuai SDD fitur #4: highlight menu sesuai seksi yang terlihat
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('a.nav-link');
|
||||
|
||||
const sectionObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
navLinks.forEach(link => {
|
||||
link.classList.remove('active');
|
||||
if (link.getAttribute('href') === `#${entry.target.id}`) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: '-30% 0px -65% 0px' }
|
||||
);
|
||||
|
||||
sections.forEach(s => sectionObserver.observe(s));
|
||||
|
||||
|
||||
// ─── Back to Top visibility ────────────────────────
|
||||
const backToTop = document.getElementById('back-to-top');
|
||||
if (backToTop) {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 400) {
|
||||
backToTop.classList.remove('opacity-0', 'pointer-events-none');
|
||||
backToTop.classList.add('opacity-100');
|
||||
} else {
|
||||
backToTop.classList.add('opacity-0', 'pointer-events-none');
|
||||
backToTop.classList.remove('opacity-100');
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
backToTop.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Contact Us Modal (Hubungi Kami) ────────────────
|
||||
const btnOpenContact = document.getElementById('btn-open-contact');
|
||||
const contactOverlay = document.getElementById('contact-overlay');
|
||||
const contactModal = document.getElementById('contact-modal');
|
||||
const contactClose = document.getElementById('contact-close');
|
||||
const contactBackdrop = document.getElementById('contact-backdrop');
|
||||
|
||||
const openContactModal = () => {
|
||||
if (!contactOverlay) return;
|
||||
contactOverlay.classList.remove('hidden');
|
||||
contactOverlay.classList.add('flex');
|
||||
// Animasi muncul
|
||||
setTimeout(() => {
|
||||
contactOverlay.classList.remove('opacity-0');
|
||||
contactModal.classList.remove('scale-95');
|
||||
contactModal.classList.add('scale-100');
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const closeContactModal = () => {
|
||||
if (!contactOverlay) return;
|
||||
contactOverlay.classList.add('opacity-0');
|
||||
contactModal.classList.remove('scale-100');
|
||||
contactModal.classList.add('scale-95');
|
||||
// Tunggu animasi selesai baru di-hide
|
||||
setTimeout(() => {
|
||||
contactOverlay.classList.remove('flex');
|
||||
contactOverlay.classList.add('hidden');
|
||||
}, 300);
|
||||
};
|
||||
|
||||
if (btnOpenContact) btnOpenContact.addEventListener('click', openContactModal);
|
||||
if (contactClose) contactClose.addEventListener('click', closeContactModal);
|
||||
if (contactBackdrop) contactBackdrop.addEventListener('click', closeContactModal);
|
||||
|
||||
}); // end DOMContentLoaded
|
||||
@@ -1,202 +0,0 @@
|
||||
// =====================================================
|
||||
// popup.js — Modal Detail Beasiswa
|
||||
// Tutup via: tombol ×, klik overlay, tekan Escape
|
||||
// + DUKUNGAN MULTI-BAHASA (ID/EN)
|
||||
// =====================================================
|
||||
|
||||
const overlay = document.getElementById('popup-overlay');
|
||||
const modal = document.getElementById('popup-modal');
|
||||
const closeBtn = document.getElementById('popup-close');
|
||||
const popupBody = document.getElementById('popup-content');
|
||||
|
||||
let currentOpenId = null; // Menyimpan status beasiswa mana yang sedang dibuka
|
||||
|
||||
// Bertanya langsung ke mesin utama (localization.js)
|
||||
function getPopupLang() {
|
||||
if (typeof window.getLang === 'function') {
|
||||
return window.getLang();
|
||||
}
|
||||
return localStorage.getItem('if_untan_lang') || 'id';
|
||||
}
|
||||
|
||||
// Format tanggal dinamis (ID / EN)
|
||||
function fmtDate(str, lang) {
|
||||
if (!str) return '—';
|
||||
const d = new Date(str);
|
||||
if (isNaN(d.getTime())) return str;
|
||||
return d.toLocaleDateString(lang === 'en' ? 'en-US' : 'id-ID', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Kamus Teks Statis Modal
|
||||
const popupDict = {
|
||||
id: {
|
||||
req_empty: 'Syarat belum tersedia. Kunjungi sumber resmi.',
|
||||
req_title: 'Syarat Umum',
|
||||
open_date: 'Tanggal Buka',
|
||||
close_date: 'Tanggal Tutup',
|
||||
contact: 'Kontak',
|
||||
more_info: 'Info Lebih Lanjut',
|
||||
no_link: 'Tautan tidak tersedia. Silakan hubungi admin prodi.',
|
||||
status_open: 'Buka',
|
||||
status_closed: 'Ditutup'
|
||||
},
|
||||
en: {
|
||||
req_empty: 'Requirements not available. Visit official source.',
|
||||
req_title: 'General Requirements',
|
||||
open_date: 'Open Date',
|
||||
close_date: 'Closing Date',
|
||||
contact: 'Contact',
|
||||
more_info: 'More Information',
|
||||
no_link: 'Link unavailable. Please contact department admin.',
|
||||
status_open: 'Open',
|
||||
status_closed: 'Closed'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Buka popup modal untuk beasiswa dengan ID tertentu
|
||||
*/
|
||||
function openPopup(id) {
|
||||
currentOpenId = id;
|
||||
const data = window.beasiswaData || [];
|
||||
const bsw = data.find(b => b.id === id);
|
||||
if (!bsw) return;
|
||||
|
||||
const lang = getPopupLang();
|
||||
const d = popupDict[lang];
|
||||
|
||||
const isBuka = bsw.status === 'Buka' || bsw.status === 'Dibuka';
|
||||
const badgeClass = isBuka ? 'badge-buka' : 'badge-ditutup';
|
||||
|
||||
// Terjemahkan Status & Kategori
|
||||
const statusText = isBuka ? d.status_open : (lang === 'en' && (bsw.status === 'Ditutup' || bsw.status === 'Tutup') ? d.status_closed : bsw.status);
|
||||
|
||||
// Menerjemahkan Kategori (Misal: "Beasiswa Swasta" -> "Private Scholarship")
|
||||
let kategoriText = bsw.kategori;
|
||||
if (lang === 'en') {
|
||||
if (bsw.kategori_en) {
|
||||
kategoriText = bsw.kategori_en;
|
||||
} else if (bsw.kategori.toLowerCase().includes('swasta')) {
|
||||
kategoriText = 'Private Scholarship';
|
||||
} else if (bsw.kategori.toLowerCase().includes('pemerintah')) {
|
||||
kategoriText = 'Government Scholarship';
|
||||
}
|
||||
}
|
||||
|
||||
// Teks Dinamis (Ambil _en dari JSON jika ada, jika tidak pakai ID)
|
||||
const namaText = (lang === 'en' && bsw.nama_en) ? bsw.nama_en : bsw.nama;
|
||||
const deskripsiText = (lang === 'en' && bsw.deskripsi_singkat_en) ? bsw.deskripsi_singkat_en : bsw.deskripsi_singkat;
|
||||
const syaratData = (lang === 'en' && bsw.syarat_umum_en) ? bsw.syarat_umum_en : (bsw.syarat_umum || []);
|
||||
|
||||
// Bangun list syarat umum
|
||||
const syaratItems = syaratData
|
||||
.map(s => `
|
||||
<li>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>${s}</span>
|
||||
</li>`)
|
||||
.join('');
|
||||
|
||||
const syaratHTML = syaratItems
|
||||
? `<ul class="syarat-list">${syaratItems}</ul>`
|
||||
: `<p style="font-size:13px;color:#94a3b8;font-style:italic;">${d.req_empty}</p>`;
|
||||
|
||||
const hasUrl = bsw.url_sumber && bsw.url_sumber !== '#';
|
||||
|
||||
// Render konten popup dengan template Anda yang dipertahankan
|
||||
popupBody.innerHTML = `
|
||||
<div class="popup-body">
|
||||
<div class="popup-kategori-row">
|
||||
<span class="badge-status ${badgeClass}">${statusText}</span>
|
||||
<span class="popup-kategori">${kategoriText}</span>
|
||||
</div>
|
||||
|
||||
<h2 id="popup-title" class="popup-title">${namaText}</h2>
|
||||
|
||||
<p class="popup-desc">${deskripsiText}</p>
|
||||
|
||||
<div class="popup-section-title">
|
||||
<i class="fas fa-list-check"></i>
|
||||
${d.req_title}
|
||||
</div>
|
||||
${syaratHTML}
|
||||
|
||||
<div class="popup-periode">
|
||||
<div class="popup-periode-item">
|
||||
<label>${d.open_date}</label>
|
||||
<span>${fmtDate(bsw.tanggal_buka, lang)}</span>
|
||||
</div>
|
||||
<div class="popup-periode-item">
|
||||
<label>${d.close_date}</label>
|
||||
<span style="${!isBuka ? 'color:#EF4444;' : ''}">${fmtDate(bsw.tanggal_tutup, lang)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${bsw.kontak ? `
|
||||
<div class="popup-kontak">
|
||||
<i class="fas fa-phone-alt"></i>
|
||||
<div>
|
||||
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px;">${d.contact}</div>
|
||||
${bsw.kontak}
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
${hasUrl
|
||||
? `<a
|
||||
href="${bsw.url_sumber}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="popup-cta"
|
||||
aria-label="${d.more_info} ${bsw.nama}"
|
||||
>
|
||||
${d.more_info}
|
||||
<i class="fas fa-external-link-alt" style="font-size:12px;"></i>
|
||||
</a>`
|
||||
: `<button class="popup-cta disabled" disabled aria-disabled="true">
|
||||
${d.more_info}
|
||||
<i class="fas fa-external-link-alt" style="font-size:12px;"></i>
|
||||
</button>
|
||||
<p style="text-align:center;font-size:11px;color:#94a3b8;margin-top:8px;">
|
||||
${d.no_link}
|
||||
</p>`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Tampilkan overlay
|
||||
overlay.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Set focus ke modal untuk aksesibilitas
|
||||
setTimeout(() => modal.focus(), 50);
|
||||
}
|
||||
|
||||
/** Tutup popup modal */
|
||||
function closePopup() {
|
||||
currentOpenId = null;
|
||||
overlay.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// === Event Listeners untuk menutup popup ===
|
||||
closeBtn?.addEventListener('click', closePopup);
|
||||
document.getElementById('popup-backdrop')?.addEventListener('click', closePopup);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && overlay.classList.contains('show')) {
|
||||
closePopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Jika bahasa diganti SAAT popup sedang terbuka, cetak ulang isinya!
|
||||
document.addEventListener('languageChanged', () => {
|
||||
if (currentOpenId && overlay.classList.contains('show')) {
|
||||
openPopup(currentOpenId);
|
||||
}
|
||||
});
|
||||
|
||||
window.openPopup = openPopup;
|
||||
window.closePopup = closePopup;
|
||||
@@ -1,454 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 101,
|
||||
"nama": "Boustany Foundation Scholarship, MBA - Harvard University",
|
||||
"nama_en": "Boustany Foundation Scholarship, MBA - Harvard University",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S2",
|
||||
"negara": "Amerika Serikat",
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tenggat_waktu": "2027-05-31",
|
||||
"url": "https://www.schoters.com/id/beasiswa/boustany-foundation-scholarship-mba-harvard-university-1",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1541339907198-e08756dedf3f?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa bergengsi dari Boustany Foundation untuk program MBA di Harvard University, Amerika Serikat, bagi calon pemimpin bisnis dunia.",
|
||||
"deskripsi_singkat_en": "A prestigious scholarship from the Boustany Foundation for the MBA program at Harvard University, United States, for future global business leaders.",
|
||||
"syarat_umum": [
|
||||
"Diterima atau mendaftar di program MBA Harvard Business School",
|
||||
"Memiliki rekam jejak prestasi akademik dan profesional yang luar biasa",
|
||||
"Menunjukkan potensi kepemimpinan dan kontribusi internasional",
|
||||
"Melengkapi aplikasi beasiswa secara online melalui situs resmi Boustany Foundation"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Accepted or applying to the MBA program at Harvard Business School",
|
||||
"Have an exceptional track record of academic and professional achievements",
|
||||
"Demonstrate leadership potential and international contribution",
|
||||
"Complete the scholarship application online through the official Boustany Foundation website"
|
||||
],
|
||||
"kontak": "Boustany Foundation — boustany-foundation.com",
|
||||
"tags": ["S2", "Swasta", "Amerika Serikat", "Partially Funded", "MBA"]
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"nama": "La Trobe University - La Trobe High Achiever Scholarship (S1)",
|
||||
"nama_en": "La Trobe University - La Trobe High Achiever Scholarship (S1)",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S1",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2023-09-29",
|
||||
"tenggat_waktu": "2026-12-31",
|
||||
"url": "https://www.schoters.com/id/beasiswa/la-trobe-university-la-trobe-high-achiever-scholarship-2",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1523240795612-9a054b0db644?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa dari La Trobe University Australia untuk mahasiswa internasional jenjang S1 berprestasi tinggi yang ingin melanjutkan studi di Melbourne.",
|
||||
"deskripsi_singkat_en": "Scholarship from La Trobe University Australia for high-achieving international undergraduate students who wish to pursue their studies in Melbourne.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar sebagai mahasiswa internasional jenjang S1 (Undergraduate) di La Trobe University",
|
||||
"Memiliki nilai akademik yang sangat baik (High Achiever)",
|
||||
"Tidak sedang menerima beasiswa penuh lainnya dari La Trobe University",
|
||||
"Memenuhi persyaratan masuk dan kemampuan bahasa Inggris La Trobe University"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply as an international undergraduate student at La Trobe University",
|
||||
"Have an excellent academic record (High Achiever)",
|
||||
"Not currently receiving any other full scholarship from La Trobe University",
|
||||
"Meet La Trobe University's entry and English language requirements"
|
||||
],
|
||||
"kontak": "La Trobe University Admissions — latrobe.edu.au",
|
||||
"tags": ["S1", "Swasta", "Australia", "Partially Funded"]
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"nama": "Charles Sturt University - International Student Support Scholarship",
|
||||
"nama_en": "Charles Sturt University - International Student Support Scholarship",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S1",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tenggat_waktu": "2026-12-31",
|
||||
"url": "https://www.schoters.com/id/beasiswa/charles-sturt-university-international-student-support-scholarship-3",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1560472354-b33ff0c44a43?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa dukungan dari Charles Sturt University bagi mahasiswa internasional jenjang S1 yang mendaftar di kampus yang berlokasi di New South Wales, Australia.",
|
||||
"deskripsi_singkat_en": "Support scholarship from Charles Sturt University for international undergraduate students enrolling at a campus located in New South Wales, Australia.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar sebagai mahasiswa internasional penuh waktu (full-time) jenjang S1",
|
||||
"Memenuhi persyaratan penerimaan akademik Charles Sturt University",
|
||||
"Memiliki kemampuan Bahasa Inggris yang memadai (IELTS/TOEFL)",
|
||||
"Mendaftar melalui jalur penerimaan internasional resmi"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply as a full-time international undergraduate student",
|
||||
"Meet Charles Sturt University's academic admission requirements",
|
||||
"Have adequate English language proficiency (IELTS/TOEFL)",
|
||||
"Apply through the official international admission pathway"
|
||||
],
|
||||
"kontak": "Charles Sturt University International — csu.edu.au",
|
||||
"tags": ["S1", "Swasta", "Australia", "Partially Funded"]
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"nama": "La Trobe University - La Trobe High Achiever Scholarship (S2)",
|
||||
"nama_en": "La Trobe University - La Trobe High Achiever Scholarship (S2)",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S2",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2023-09-29",
|
||||
"tenggat_waktu": "2026-12-31",
|
||||
"url": "https://www.schoters.com/id/beasiswa/la-trobe-university-la-trobe-high-achiever-scholarship-4",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa dari La Trobe University Australia untuk mahasiswa internasional jenjang S2 (Postgraduate) dengan pencapaian akademik tinggi.",
|
||||
"deskripsi_singkat_en": "Scholarship from La Trobe University Australia for international postgraduate students with high academic achievements.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar sebagai mahasiswa internasional jenjang S2 di La Trobe University",
|
||||
"Memiliki nilai akademik yang sangat baik di jenjang sarjana sebelumnya",
|
||||
"Memenuhi persyaratan bahasa Inggris yang ditetapkan",
|
||||
"Tidak sedang menerima beasiswa lain dari universitas yang sama"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply as an international postgraduate student at La Trobe University",
|
||||
"Have excellent academic grades in the previous undergraduate degree",
|
||||
"Meet the specified English language requirements",
|
||||
"Not currently receiving another scholarship from the same university"
|
||||
],
|
||||
"kontak": "La Trobe University Admissions — latrobe.edu.au",
|
||||
"tags": ["S2", "Swasta", "Australia", "Partially Funded"]
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"nama": "ETH Zurich - Excellence Scholarship & Opportunity Programme (ESOP)",
|
||||
"nama_en": "ETH Zurich - Excellence Scholarship & Opportunity Programme (ESOP)",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Pemerintah",
|
||||
"jenjang": "S2",
|
||||
"negara": "Swiss",
|
||||
"tanggal_buka": "2026-11-01",
|
||||
"tenggat_waktu": "2026-11-30",
|
||||
"url": "https://www.schoters.com/id/beasiswa/eth-zurich-excellence-scholarship-opportunity-programme-esop-5",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa bergengsi dari ETH Zurich (salah satu universitas teknik terbaik dunia) untuk mahasiswa S2 internasional yang unggul secara akademik.",
|
||||
"deskripsi_singkat_en": "Prestigious scholarship from ETH Zurich (one of the world's best engineering universities) for academically outstanding international master's students.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar program Master (S2) di ETH Zurich",
|
||||
"Lulus ujian penerimaan ETH Zurich atau memiliki setara gelar sarjana yang diakui",
|
||||
"Menunjukkan prestasi akademik luar biasa (biasanya masuk 10% terbaik angkatan)",
|
||||
"Melengkapi formulir aplikasi beasiswa ESOP yang terpisah dari aplikasi studi",
|
||||
"Mendapatkan surat rekomendasi dari akademisi berwenang"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply for a Master's program at ETH Zurich",
|
||||
"Pass the ETH Zurich entrance exam or hold a recognized equivalent bachelor's degree",
|
||||
"Demonstrate outstanding academic performance (usually top 10% of the cohort)",
|
||||
"Complete the ESOP scholarship application form separately from the study application",
|
||||
"Obtain a recommendation letter from authorized academics"
|
||||
],
|
||||
"kontak": "ETH Zurich Scholarship Office — ethz.ch",
|
||||
"tags": ["S2", "Pemerintah", "Swiss", "Fully Funded", "Prestasi"]
|
||||
},
|
||||
{
|
||||
"id": 106,
|
||||
"nama": "STUST - Scholarship for International Students (S3, Fully Funded)",
|
||||
"nama_en": "STUST - Scholarship for International Students (S3, Fully Funded)",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S3",
|
||||
"negara": "Taiwan",
|
||||
"tanggal_buka": "2026-09-15",
|
||||
"tenggat_waktu": "2026-11-30",
|
||||
"url": "https://www.schoters.com/id/beasiswa/southern-taiwan-university-of-science-and-technology-the-stust-scholarship-for-international-students-6",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1580582932707-520aed937b7b?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa penuh (fully funded) dari Southern Taiwan University of Science and Technology untuk mahasiswa internasional jenjang S3 (Doktoral).",
|
||||
"deskripsi_singkat_en": "Fully funded scholarship from Southern Taiwan University of Science and Technology for international doctoral students.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar program Doktoral (S3) di STUST, Taiwan",
|
||||
"Memiliki gelar magister (S2) yang diakui",
|
||||
"Memenuhi persyaratan kemampuan bahasa Inggris atau Mandarin",
|
||||
"Menyerahkan proposal penelitian yang relevan dengan bidang studi yang dipilih",
|
||||
"Mendapatkan persetujuan dari calon pembimbing (supervisor) di STUST"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply for a Doctoral program at STUST, Taiwan",
|
||||
"Hold a recognized Master's degree",
|
||||
"Meet the English or Mandarin language proficiency requirements",
|
||||
"Submit a research proposal relevant to the chosen field of study",
|
||||
"Obtain approval from a prospective supervisor at STUST"
|
||||
],
|
||||
"kontak": "STUST International Affairs Office — stust.edu.tw",
|
||||
"tags": ["S3", "Swasta", "Taiwan", "Fully Funded"]
|
||||
},
|
||||
{
|
||||
"id": 107,
|
||||
"nama": "STUST - Scholarship for International Students (S2, Partially Funded)",
|
||||
"nama_en": "STUST - Scholarship for International Students (S2, Partially Funded)",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S2",
|
||||
"negara": "Taiwan",
|
||||
"tanggal_buka": "2026-09-15",
|
||||
"tenggat_waktu": "2026-11-30",
|
||||
"url": "https://www.schoters.com/id/beasiswa/southern-taiwan-university-of-science-and-technology-the-stust-scholarship-for-international-students-7",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1606761568499-6d2451b23c66?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa parsial (partially funded) dari Southern Taiwan University of Science and Technology untuk mahasiswa internasional jenjang S2 (Magister).",
|
||||
"deskripsi_singkat_en": "Partially funded scholarship from Southern Taiwan University of Science and Technology for international master's students.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar program Magister (S2) di STUST, Taiwan",
|
||||
"Memiliki gelar sarjana (S1) yang diakui",
|
||||
"Memenuhi persyaratan kemampuan bahasa Inggris atau Mandarin",
|
||||
"Melengkapi berkas aplikasi secara online melalui portal resmi STUST",
|
||||
"Menyertakan transkrip nilai dan surat motivasi"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply for a Master's program at STUST, Taiwan",
|
||||
"Hold a recognized Bachelor's degree",
|
||||
"Meet the English or Mandarin language proficiency requirements",
|
||||
"Complete the application file online through the official STUST portal",
|
||||
"Include academic transcripts and a motivation letter"
|
||||
],
|
||||
"kontak": "STUST International Affairs Office — stust.edu.tw",
|
||||
"tags": ["S2", "Swasta", "Taiwan", "Partially Funded"]
|
||||
},
|
||||
{
|
||||
"id": 108,
|
||||
"nama": "University of Melbourne - Melbourne International Undergraduate Scholarship",
|
||||
"nama_en": "University of Melbourne - Melbourne International Undergraduate Scholarship",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S1",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tenggat_waktu": "2026-10-31",
|
||||
"url": "https://www.schoters.com/id/beasiswa/university-of-melbourne-melbourne-international-undergraduate-scholarship-8",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1504711434969-e33886168f5c?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa untuk mahasiswa internasional berprestasi yang mendaftar program S1 di University of Melbourne, salah satu universitas terbaik di Australia.",
|
||||
"deskripsi_singkat_en": "Scholarship for outstanding international students applying for an undergraduate program at the University of Melbourne, one of Australia's best universities.",
|
||||
"syarat_umum": [
|
||||
"Merupakan mahasiswa internasional (bukan warga negara Australia atau New Zealand)",
|
||||
"Mendaftar program sarjana (S1 / Undergraduate) di University of Melbourne",
|
||||
"Memiliki hasil akademik yang sangat luar biasa di tingkat sekolah menengah atas",
|
||||
"Memenuhi persyaratan kemampuan bahasa Inggris (IELTS minimal 6.5)",
|
||||
"Beasiswa diberikan otomatis berdasarkan nilai akademik, tidak perlu aplikasi terpisah"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Be an international student (not a citizen of Australia or New Zealand)",
|
||||
"Apply for an undergraduate program at the University of Melbourne",
|
||||
"Have an exceptional academic result at the high school level",
|
||||
"Meet the English language proficiency requirements (minimum IELTS 6.5)",
|
||||
"The scholarship is awarded automatically based on academic merit, no separate application required"
|
||||
],
|
||||
"kontak": "University of Melbourne Future Students — unimelb.edu.au",
|
||||
"tags": ["S1", "Swasta", "Australia", "Partially Funded", "Prestasi"]
|
||||
},
|
||||
{
|
||||
"id": 109,
|
||||
"nama": "Charles Darwin University - CDU Global Merit Scholarship",
|
||||
"nama_en": "Charles Darwin University - CDU Global Merit Scholarship",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S1",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tenggat_waktu": "2026-10-09",
|
||||
"url": "https://www.schoters.com/id/beasiswa/charles-darwin-university-cdu-global-merit-scholarship-9",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1434030216411-0b793f4b4173?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa merit global dari Charles Darwin University (CDU) di Darwin, Australia, untuk mahasiswa internasional S1 yang memiliki rekam jejak akademik yang kuat.",
|
||||
"deskripsi_singkat_en": "Global merit scholarship from Charles Darwin University (CDU) in Darwin, Australia, for international undergraduate students with a strong academic track record.",
|
||||
"syarat_umum": [
|
||||
"Mendaftar sebagai mahasiswa internasional baru (new international student) di CDU",
|
||||
"Mendaftar untuk program sarjana (Undergraduate/S1) penuh waktu",
|
||||
"Memiliki nilai ujian akhir sekolah yang baik (dikonversi ke skala CDU)",
|
||||
"Memenuhi persyaratan bahasa Inggris (IELTS 6.0 atau setara)",
|
||||
"Tidak sedang menerima potongan biaya kuliah lainnya dari CDU"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Apply as a new international student at CDU",
|
||||
"Apply for a full-time undergraduate program",
|
||||
"Have a good high school final exam score (converted to CDU scale)",
|
||||
"Meet the English language requirements (IELTS 6.0 or equivalent)",
|
||||
"Not currently receiving any other tuition fee discount from CDU"
|
||||
],
|
||||
"kontak": "CDU International — cdu.edu.au",
|
||||
"tags": ["S1", "Swasta", "Australia", "Partially Funded"]
|
||||
},
|
||||
{
|
||||
"id": 110,
|
||||
"nama": "Deakin University - Vice-Chancellor's International Scholarship",
|
||||
"nama_en": "Deakin University - Vice-Chancellor's International Scholarship",
|
||||
"status": "Buka",
|
||||
<<<<<<< HEAD
|
||||
"tipe_pendanaan": "Swasta",
|
||||
"jenjang": "S1 & S2",
|
||||
"negara": "Australia",
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tenggat_waktu": "2026-09-25",
|
||||
"url": "https://www.schoters.com/id/beasiswa/deakin-university-vice-chancellor-s-international-scholarship-10",
|
||||
"thumbnail": "https://images.unsplash.com/photo-1541339907198-e08756dedf3f?w=400&q=80&auto=format&fit=crop",
|
||||
=======
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
"deskripsi_singkat": "Beasiswa bergengsi dari Wakil Rektor (Vice-Chancellor) Deakin University untuk mahasiswa internasional terbaik yang akan menempuh studi S1 atau S2 di Australia.",
|
||||
"deskripsi_singkat_en": "Prestigious Vice-Chancellor's scholarship from Deakin University for the best international students pursuing an undergraduate or postgraduate degree in Australia.",
|
||||
"syarat_umum": [
|
||||
"Merupakan mahasiswa internasional (bukan warga negara Australia/New Zealand)",
|
||||
"Mendaftar untuk program S1 (Undergraduate) atau S2 (Postgraduate Coursework) di Deakin",
|
||||
"Memiliki rekam jejak akademik yang sangat baik dan konsisten",
|
||||
"Memenuhi persyaratan masuk dan kemampuan bahasa Inggris Deakin University",
|
||||
"Mengajukan aplikasi beasiswa sebelum tanggal penutupan yang ditetapkan"
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Be an international student (not an Australia/New Zealand citizen)",
|
||||
"Apply for an undergraduate or postgraduate coursework program at Deakin",
|
||||
"Have a consistent and excellent academic track record",
|
||||
"Meet Deakin University's entry and English language requirements",
|
||||
"Submit the scholarship application before the specified closing date"
|
||||
],
|
||||
"kontak": "Deakin University International — deakin.edu.au",
|
||||
<<<<<<< HEAD
|
||||
"tags": ["S1 & S2", "Swasta", "Australia", "Partially Funded", "Prestasi"]
|
||||
}
|
||||
]
|
||||
=======
|
||||
"tanggal_buka": "2026-05-21",
|
||||
"tanggal_tutup": "2026-09-25",
|
||||
"url_sumber": "https://www.schoters.com/id/beasiswa/deakin-university-vice-chancellor-s-international-scholarship-10",
|
||||
"tags": ["S1 & S2", "S2", "Swasta", "Australia", "Partially Funded", "Prestasi"]
|
||||
},
|
||||
{
|
||||
"id": "bsw-011",
|
||||
"nama": "Djarum Beasiswa Plus 2026/2027",
|
||||
"nama_en": "Djarum Plus Scholarship 2026/2027",
|
||||
"kategori": "Swasta",
|
||||
"status": "Buka",
|
||||
"deskripsi_singkat": "Program beasiswa prestasi dari Djarum Foundation untuk mahasiswa S1.",
|
||||
"deskripsi_singkat_en": "Merit-based scholarship program from Djarum Foundation for undergraduate students.",
|
||||
"syarat_umum": [
|
||||
"Mahasiswa S1/D4 semester 4 dari semua disiplin ilmu.",
|
||||
"IPK minimum 3.00 pada semester 3."
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"4th-semester undergraduate (S1/D4) students from all disciplines.",
|
||||
"Minimum GPA of 3.00 in the 3rd semester."
|
||||
],
|
||||
"kontak": "https://register.djarumbeasiswaplus.org/",
|
||||
"tanggal_buka": "2026-04-08",
|
||||
"tanggal_tutup": "2026-06-10",
|
||||
"url_sumber": "https://register.djarumbeasiswaplus.org/",
|
||||
"tags": ["S1", "D4", "Swasta", "Indonesia"]
|
||||
},
|
||||
{
|
||||
"id": "bsw-012",
|
||||
"nama": "Beasiswa Sobat Bumi Kalimantan 2026",
|
||||
"nama_en": "Sobat Bumi Kalimantan Scholarship 2026",
|
||||
"kategori": "Swasta",
|
||||
"status": "Buka",
|
||||
"deskripsi_singkat": "Beasiswa dari Pertamina Foundation khusus mahasiswa S1 di wilayah Kalimantan.",
|
||||
"deskripsi_singkat_en": "Scholarship from Pertamina Foundation specifically for undergraduate students in the Kalimantan region.",
|
||||
"syarat_umum": [
|
||||
"Mahasiswa S1 aktif yang berdomisili atau kuliah di Kalimantan.",
|
||||
"Memiliki kepedulian terhadap lingkungan hidup."
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Active undergraduate students domiciled or studying in Kalimantan.",
|
||||
"Demonstrate care and commitment to the environment."
|
||||
],
|
||||
"kontak": "https://pertaminafoundation.org/",
|
||||
"tanggal_buka": "2026-05-02",
|
||||
"tanggal_tutup": "2026-07-01",
|
||||
"url_sumber": "https://pertaminafoundation.org/",
|
||||
"tags": ["S1", "Swasta", "Indonesia"]
|
||||
},
|
||||
{
|
||||
"id": "bsw-013",
|
||||
"nama": "Beasiswa Sehat Kemenkes 2026",
|
||||
"nama_en": "Ministry of Health (Sehat) Scholarship 2026",
|
||||
"kategori": "Pemerintah",
|
||||
"status": "Segera Dibuka",
|
||||
"deskripsi_singkat": "Program bantuan biaya pendidikan dari Kementerian Kesehatan untuk jenjang D3, D4, dan Profesi.",
|
||||
"deskripsi_singkat_en": "Educational assistance program from the Ministry of Health for D3, D4, and Professional degrees.",
|
||||
"syarat_umum": [
|
||||
"Mahasiswa aktif jenjang D3, D4, atau Profesi di bidang kesehatan.",
|
||||
"Memenuhi kualifikasi dan standar nilai dari Kementerian Kesehatan."
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Active D3, D4, or Professional degree students in the health sector.",
|
||||
"Meet the qualifications and academic standards of the Ministry of Health."
|
||||
],
|
||||
"kontak": "https://sibk.kemkes.go.id/",
|
||||
"tanggal_buka": "2026-06-08",
|
||||
"tanggal_tutup": "2026-06-30",
|
||||
"url_sumber": "https://sibk.kemkes.go.id/",
|
||||
"tags": ["D3", "D4", "Profesi", "Pemerintah", "Indonesia"]
|
||||
},
|
||||
{
|
||||
"id": "bsw-014",
|
||||
"nama": "Beasiswa SDM Sawit 2026",
|
||||
"nama_en": "Palm Oil Human Resources Scholarship 2026",
|
||||
"kategori": "Pemerintah",
|
||||
"status": "Buka",
|
||||
"deskripsi_singkat": "Beasiswa pendanaan BPDPKS untuk pengembangan SDM Perkebunan Kelapa Sawit (D1-D4, S1).",
|
||||
"deskripsi_singkat_en": "BPDPKS funding scholarship for the development of Palm Oil Plantation Human Resources (D1-D4, S1).",
|
||||
"syarat_umum": [
|
||||
"Terbuka untuk lulusan SMA/SMK/sederajat yang melanjutkan ke jenjang D1-D4 atau S1.",
|
||||
"Bersedia ditempatkan atau mengabdi di sektor perkebunan kelapa sawit."
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"Open to high school/vocational school graduates continuing to D1-D4 or S1 levels.",
|
||||
"Willing to be placed or serve in the palm oil plantation sector."
|
||||
],
|
||||
"kontak": "https://sdmperkebunan.bpdp.or.id/",
|
||||
"tanggal_buka": "2026-06-03",
|
||||
"tanggal_tutup": "2026-06-20",
|
||||
"url_sumber": "https://sdmperkebunan.bpdp.or.id/",
|
||||
"tags": ["S1", "D1", "D3", "D4", "Pemerintah", "Indonesia"]
|
||||
},
|
||||
{
|
||||
"id": "bsw-015",
|
||||
"nama": "Beasiswa PPBP/PPTI BCA 2027",
|
||||
"nama_en": "BCA PPBP/PPTI Scholarship 2027",
|
||||
"kategori": "Swasta",
|
||||
"status": "Buka",
|
||||
"deskripsi_singkat": "Program Pendidikan Bisnis dan Perbankan (PPBP) serta Teknik Informatika (PPTI) dari BCA.",
|
||||
"deskripsi_singkat_en": "Business and Banking Education Program (PPBP) and Informatics Engineering (PPTI) from BCA.",
|
||||
"syarat_umum": [
|
||||
"Lulusan SMA/SMK/sederajat yang berprestasi.",
|
||||
"Lulus seleksi administrasi dan serangkaian tes dari BCA."
|
||||
],
|
||||
"syarat_umum_en": [
|
||||
"High-achieving high school/vocational school graduates.",
|
||||
"Pass the administrative selection and a series of tests from BCA."
|
||||
],
|
||||
"kontak": "https://bcafinance.co.id/Pendaftaran-Beasiswa-BCA-Finance-Peduli-2026-Dibuka",
|
||||
"tanggal_buka": "2026-05-01",
|
||||
"tanggal_tutup": "2026-10-20",
|
||||
"url_sumber": "https://bcafinance.co.id/Pendaftaran-Beasiswa-BCA-Finance-Peduli-2026-Dibuka",
|
||||
"tags": ["S1", "Swasta", "Indonesia"]
|
||||
}
|
||||
|
||||
]
|
||||
>>>>>>> b5bc7bad0c7de6e63a60018da0fe06e923bf094c
|
||||
@@ -1,11 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "tsm-001",
|
||||
"nama": "Violaine Gunawan",
|
||||
"foto": "https://ui-avatars.com/api/?name=Violaine+Gunawan&background=111a2f&color=fff&size=200&rounded=true",
|
||||
"nama_beasiswa": "BEASISWA BAKTI BCA 2026",
|
||||
"nama_beasiswa_en": "BAKTI BCA SCHOLARSHIP 2026",
|
||||
"kutipan": "Beasiswa ini sangat berdampak besar. Tantangannya menyeimbangkan kuliah, asisten praktikum, organisasi, dan projek besar beasiswa. Namun, relasi di kampus dan masyarakat jadi bertambah luas. Tips: Jangan takut mencoba, pahami fokus beasiswa, sesuaikan visi misi, dan aktiflah berorganisasi!",
|
||||
"kutipan_en": "This scholarship has a massive impact. The challenge is balancing college, practicum assistant duties, organizations, and major scholarship projects. However, connections on campus and in society have expanded. Tip: Don't be afraid to try, understand the scholarship's focus, align your vision and mission, and be active in organizations!"
|
||||
}
|
||||
]
|
||||
@@ -1,27 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" class="scroll-smooth">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Group 10 – ApeNameTeamE | Informatika UNTAN</title>
|
||||
<title>Group 10 – Ape Name Tim E | Informatika UNTAN</title>
|
||||
<link rel="stylesheet" href="../../assets/global/output.css">
|
||||
<link rel="stylesheet" href="assets/css/style.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- AOS.js -->
|
||||
<link href="https://unpkg.com/aos@2.3.4/dist/aos.css" rel="stylesheet" />
|
||||
|
||||
<!-- Swiper.js -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
|
||||
|
||||
|
||||
<style>
|
||||
<style>
|
||||
/* ===== MOBILE NAV STABILITY FIX =====
|
||||
Prevents logo bar from collapsing/shifting on Android when
|
||||
address bar hides/shows and triggers viewport reflow.
|
||||
@@ -305,7 +293,6 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-slate-800 antialiased bg-white selection:bg-untan-yellow selection:text-black">
|
||||
|
||||
<a href="#main-content" class="skip-link" data-i18n="skip_to_content">Langsung ke Konten Utama</a>
|
||||
@@ -1152,262 +1139,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<div class="ticker-wrapper" aria-label="Pengumuman beasiswa terbaru" role="marquee">
|
||||
<!-- Badge block -->
|
||||
<div class="ticker-badge-block">
|
||||
<span class="ticker-badge" aria-hidden="true">INFO TERBARU</span>
|
||||
</div>
|
||||
<!-- Scroll area: inner wrapper berisi 2 copy teks, dianimasikan bersamaan -->
|
||||
<div class="ticker-scroll-area">
|
||||
<div class="ticker-inner" aria-live="off">
|
||||
<span class="ticker-track">Selamat datang di antarmuka situs web Informatika terbaru!
|
||||
| Pendaftaran
|
||||
kolaborasi riset semester genap telah dibuka. | Pantau publikasi dosen dan
|
||||
mahasiswa secara
|
||||
real-time. | Beasiswa KIP Kuliah 2026 dibuka — Daftar sekarang!
|
||||
| Beasiswa LPDP S2
|
||||
tersedia hingga September 2026. </span>
|
||||
<span class="ticker-track" aria-hidden="true">Selamat datang di antarmuka situs web
|
||||
Informatika terbaru!
|
||||
| Pendaftaran kolaborasi riset semester genap telah dibuka. |
|
||||
Pantau publikasi dosen
|
||||
dan mahasiswa secara real-time. | Beasiswa KIP Kuliah 2026 dibuka — Daftar
|
||||
sekarang! |
|
||||
Beasiswa LPDP S2 tersedia hingga September 2026. </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================
|
||||
HERO SECTION (#hero)
|
||||
================================================ -->
|
||||
<section id="hero" class="hero-section" aria-label="Header halaman beasiswa">
|
||||
<div class="hero-bg"></div>
|
||||
|
||||
<div class="hero-content max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||||
<a href="#" data-i18n="breadcrumb_home">Beranda</a>
|
||||
<span class="sep"><i class="fas fa-chevron-right" style="font-size:7px;"></i></span>
|
||||
<a href="#" data-i18n="ape_breadcrumb_profile">Pendidikan</a>
|
||||
<span class="sep"><i class="fas fa-chevron-right" style="font-size:7px;"></i></span>
|
||||
<span class="current" data-i18n="ape_breadcrumb_scholarship">Beasiswa</span>
|
||||
</nav>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-10 lg:gap-20 items-center">
|
||||
|
||||
<!-- Left: Text Content -->
|
||||
<div data-aos="fade-right" data-aos-duration="700">
|
||||
<!-- Badge (outline style seperti Figma) -->
|
||||
<div class="hero-badge">
|
||||
<span data-i18n="ape_hero_badge">PROGRAM BEASISWA</span>
|
||||
</div>
|
||||
|
||||
<!-- H1 -->
|
||||
<h1 class="hero-title" data-i18n-html="ape_hero_title">
|
||||
Beasiswa Studi &<br>Penelitian
|
||||
</h1>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<p class="hero-subtitle" data-i18n="ape_hero_subtitle">
|
||||
Teknik Informatika Universitas Tanjungpura
|
||||
</p>
|
||||
|
||||
<!-- Description (sesuai teks Figma) -->
|
||||
<p class="hero-desc" data-i18n="ape_hero_desc">
|
||||
Temukan berbagai peluang beasiswa untuk mendukung perjalanan akademik dan penelitian Anda.
|
||||
</p>
|
||||
|
||||
<!-- CTA Button (kuning, teks gelap — sesuai Figma) -->
|
||||
<a href="#directory" class="hero-cta" aria-label="Lihat daftar beasiswa">
|
||||
<span data-i18n="ape_hero_cta">Lihat Daftar Beasiswa</span>
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Right: Hero Image -->
|
||||
<div class="hidden lg:block" data-aos="fade-left" data-aos-delay="150"
|
||||
data-aos-duration="700">
|
||||
<div class="hero-image-wrap">
|
||||
<img src="assets/images/hero/mahasiswa.png"
|
||||
alt="Mahasiswa Informatika UNTAN berprestasi" width="480" height="400"
|
||||
onerror="this.src='https://images.unsplash.com/photo-1523050854058-8df90110c9f1?w=500&q=80'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================
|
||||
STATISTIK COUNTER (#statistics)
|
||||
================================================ -->
|
||||
<section id="statistics" class="stats-section" aria-label="Statistik penerima beasiswa">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-3 divide-x divide-white/20">
|
||||
|
||||
<div class="stat-item" data-aos="fade-up" data-aos-delay="0">
|
||||
<div class="stat-number" data-counter="20" id="ctr-total">0</div>
|
||||
<div class="stat-label" data-i18n-html="ape_stats_total">
|
||||
Total Mahasiswa Penerima<br>Beasiswa Semester Ini
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item" data-aos="fade-up" data-aos-delay="100">
|
||||
<div class="stat-number" data-counter="12" id="ctr-s1">0</div>
|
||||
<div class="stat-label" data-i18n-html="ape_stats_s1">
|
||||
Total Penerima Beasiswa<br>Khusus S1
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item" data-aos="fade-up" data-aos-delay="200">
|
||||
<div class="stat-number" data-counter="8" id="ctr-studi">0</div>
|
||||
<div class="stat-label" data-i18n-html="ape_stats_studi">
|
||||
Total Penerima Beasiswa<br>Studi Lanjut
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================
|
||||
DIREKTORI BEASISWA (#directory)
|
||||
================================================ -->
|
||||
<section id="directory" class="py-16 bg-white" aria-label="Direktori beasiswa">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- Section Header -->
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 mb-10">
|
||||
<div data-aos="fade-right">
|
||||
<!-- Label Badge "DAFTAR LENGKAP" -->
|
||||
<div class="section-badge mb-3">
|
||||
<span data-i18n="ape_dir_label">DAFTAR LENGKAP</span>
|
||||
</div>
|
||||
<h2 class="section-title" data-i18n="ape_dir_title">Daftar Beasiswa</h2>
|
||||
<p class="section-desc" data-i18n="ape_dir_desc">
|
||||
Temukan beragam beasiswa dari berbagai sumber resmi yang tersedia untuk mahasiswa aktif.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter & Search Row (UC-02, UC-03) -->
|
||||
<div class="flex flex-col lg:flex-row gap-4 mb-8" data-aos="fade-up" data-aos-delay="50">
|
||||
<!-- Filter Tabs -->
|
||||
<div class="filter-tabs flex-1" id="filter-tabs" role="tablist"
|
||||
aria-label="Filter beasiswa">
|
||||
<button class="filter-tab active" data-filter="semua" role="tab" aria-selected="true"
|
||||
data-i18n="filter_all">Semua</button>
|
||||
<button class="filter-tab" data-filter="s1" role="tab" aria-selected="false"
|
||||
data-i18n="ape_filter_s1">S1</button>
|
||||
<button class="filter-tab" data-filter="studi_lanjut" role="tab" aria-selected="false"
|
||||
data-i18n="ape_filter_studi">Studi Lanjut</button>
|
||||
</div>
|
||||
|
||||
<!-- Real-Time Search -->
|
||||
<div class="flex flex-col sm:flex-row gap-3 flex-shrink-0">
|
||||
<div class="search-wrapper w-full sm:w-64">
|
||||
<i class="fas fa-search search-icon" aria-hidden="true"></i>
|
||||
<input type="search" id="search-input" class="search-input"
|
||||
placeholder="Cari beasiswa..." data-i18n-placeholder="ape_search_placeholder"
|
||||
aria-label="Cari beasiswa" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cards Grid (rendered by filter.js) -->
|
||||
<div id="beasiswa-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" role="list"
|
||||
aria-label="Daftar kartu beasiswa">
|
||||
<!-- Skeleton loading state -->
|
||||
<div class="col-span-full flex flex-col items-center justify-center py-20 text-gray-300">
|
||||
<i class="fas fa-spinner fa-spin text-4xl mb-4"></i>
|
||||
<p class="text-sm font-medium">Memuat data beasiswa...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination (rendered by filter.js) -->
|
||||
<div id="pagination" class="pagination-container" role="navigation"
|
||||
aria-label="Navigasi halaman beasiswa"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================
|
||||
POPUP MODAL DETAIL BEASISWA (UC-04, UC-05)
|
||||
================================================ -->
|
||||
<div id="popup-overlay" role="dialog" aria-modal="true" aria-labelledby="popup-title">
|
||||
<!-- Backdrop (klik untuk tutup - UC-04) -->
|
||||
<div id="popup-backdrop" class="popup-backdrop"></div>
|
||||
|
||||
<!-- Modal Card -->
|
||||
<div id="popup-modal" class="popup-modal" tabindex="-1">
|
||||
<!-- Close Button (×) -->
|
||||
<button id="popup-close" class="popup-close" aria-label="Tutup detail beasiswa">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
||||
<!-- Content (rendered by popup.js) -->
|
||||
<div id="popup-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================
|
||||
TESTIMONI (#testimonial) — UC-07
|
||||
================================================ -->
|
||||
<section id="testimonial" class="testimonials-section" aria-label="Testimoni penerima beasiswa">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-12" data-aos="fade-up">
|
||||
<h2 class="section-title text-center" data-i18n="ape_testimonial_title">Apa Kata Mereka?</h2>
|
||||
<p class="section-desc mx-auto text-center" data-i18n="ape_testimonial_desc">
|
||||
Cerita nyata dari mahasiswa yang telah meraih beasiswa
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Swiper Carousel -->
|
||||
<div class="swiper testimonial-swiper" data-aos="fade-up" data-aos-delay="100">
|
||||
<div class="swiper-wrapper pb-14" id="testimonial-wrapper">
|
||||
<!-- Rendered by carousel.js -->
|
||||
</div>
|
||||
<div class="swiper-pagination testimonial-pagination"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ================================================
|
||||
CTA SECTION (#cta) — UC-06
|
||||
================================================ -->
|
||||
<section id="cta" class="cta-section" aria-label="Hubungi admin beasiswa">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="cta-card" data-aos="zoom-in" data-aos-duration="600">
|
||||
<h2 class="cta-title" data-i18n="ape_cta_title">
|
||||
Ingin Mengetahui Info Lebih Lanjut?
|
||||
</h2>
|
||||
<p class="cta-desc" data-i18n="ape_cta_desc">
|
||||
Bersama kami membangun sumber daya manusia unggul melalui program beasiswa dan
|
||||
kolaborasi riset
|
||||
</p>
|
||||
<div class="cta-buttons">
|
||||
<!-- Scroll ke direktori -->
|
||||
<a href="#directory" class="cta-btn-primary" aria-label="Lihat daftar beasiswa">
|
||||
<span data-i18n="ape_cta_btn1">Daftar Beasiswa</span>
|
||||
</a>
|
||||
<!-- Kontak CTA -->
|
||||
<a id="btn-open-contact" class="cta-btn-secondary" aria-label="Hubungi admin" href="https://wa.me/+6285171655161">
|
||||
<span data-i18n="ape_cta_btn2">Hubungi Kami</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<div class="container mx-auto px-4 lg:px-8 py-12">
|
||||
<div class="mb-8 pb-8 border-b border-slate-200">
|
||||
<span class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas Halaman Statis</span>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Beasiswa</h1>
|
||||
<p class="text-slate-500 text-sm">Kelompok 10 — Ape Name Tim E · Informatika UNTAN 2026</p>
|
||||
</div>
|
||||
<div id="assignment-body" class="prose prose-slate max-w-none">
|
||||
<!-- ================= START: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
|
||||
|
||||
<p>Tulis konten halaman kelompok di sini.</p>
|
||||
<h3>Subjudul</h3>
|
||||
<p>Gunakan paragraf, list, tabel, gambar, dan elemen konten lainnya.</p>
|
||||
<!-- ================= END: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1472,23 +1215,11 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full relative z-0 mt-4 pointer-events-none flex justify-center">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration"
|
||||
class="w-full h-auto object-cover object-bottom" style="object-position: bottom;">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration" class="w-full h-auto object-cover object-bottom"
|
||||
style="object-position: bottom;">
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://unpkg.com/aos@2.3.4/dist/aos.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
|
||||
<script src="assets/js/counter.js"></script>
|
||||
<script type="module" src="assets/js/API.js"></script>
|
||||
<script src="assets/js/popup.js"></script>
|
||||
<script src="assets/js/filter.js"></script>
|
||||
<script src="assets/js/carousel.js"></script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const menuItems = document.querySelectorAll('[data-menu]');
|
||||
@@ -1581,27 +1312,27 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn = document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function () {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function () {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn= document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function() {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function() {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,203 +0,0 @@
|
||||
{
|
||||
"source": "https://www.schoters.com/id/beasiswa",
|
||||
"title": "Kalender Beasiswa - Schoters",
|
||||
"description": "Kalender dan informasi beasiswa terlengkap dari semua jenjang S1, S2, & S3 di setiap negara dan kampus impian beserta persyaratan dan proses pendaftarannya",
|
||||
"filters": {
|
||||
"jenjang": [
|
||||
{ "level": "D2", "count": 2 },
|
||||
{ "level": "D3", "count": 6 },
|
||||
{ "level": "Non-Degree", "count": 20 },
|
||||
{ "level": "S1", "count": 648 },
|
||||
{ "level": "S2", "count": 683 },
|
||||
{ "level": "S3", "count": 362 }
|
||||
],
|
||||
"tipe_pendanaan": [
|
||||
{ "type": "Fully Funded", "count": 809 },
|
||||
{ "type": "Partially Funded", "count": 590 }
|
||||
],
|
||||
"tipe_beasiswa": [
|
||||
{ "type": "Company/Institution Scholarship", "count": 237 },
|
||||
{ "type": "Government Scholarship", "count": 459 },
|
||||
{ "type": "University Scholarship", "count": 830 }
|
||||
],
|
||||
"negara": [
|
||||
{ "country": "Australia", "count": 161 },
|
||||
{ "country": "Austria", "count": 19 },
|
||||
{ "country": "Azerbaijan", "count": 6 },
|
||||
{ "country": "Belgium", "count": 38 },
|
||||
{ "country": "Brazil", "count": 2 },
|
||||
{ "country": "Brunei", "count": 5 },
|
||||
{ "country": "Canada", "count": 46 },
|
||||
{ "country": "China", "count": 114 },
|
||||
{ "country": "Czech Republic", "count": 2 },
|
||||
{ "country": "Denmark", "count": 13 },
|
||||
{ "country": "Egypt", "count": 3 },
|
||||
{ "country": "Estonia", "count": 2 },
|
||||
{ "country": "Finland", "count": 11 },
|
||||
{ "country": "France", "count": 20 },
|
||||
{ "country": "Germany", "count": 46 },
|
||||
{ "country": "Hong Kong", "count": 24 },
|
||||
{ "country": "Hungary", "count": 5 },
|
||||
{ "country": "India", "count": 2 },
|
||||
{ "country": "Indonesia", "count": 35 },
|
||||
{ "country": "Ireland", "count": 8 },
|
||||
{ "country": "Italy", "count": 19 },
|
||||
{ "country": "Japan", "count": 254 },
|
||||
{ "country": "Kazakhstan", "count": 3 },
|
||||
{ "country": "Latvia", "count": 3 },
|
||||
{ "country": "Libya", "count": 2 },
|
||||
{ "country": "Luxembourg", "count": 1 },
|
||||
{ "country": "Macau", "count": 6 },
|
||||
{ "country": "Malaysia", "count": 12 },
|
||||
{ "country": "Morocco", "count": 2 },
|
||||
{ "country": "Netherlands", "count": 21 },
|
||||
{ "country": "New Zealand", "count": 22 },
|
||||
{ "country": "Pakistan", "count": 1 },
|
||||
{ "country": "Philippines", "count": 2 },
|
||||
{ "country": "Poland", "count": 9 },
|
||||
{ "country": "Portugal", "count": 4 },
|
||||
{ "country": "Qatar", "count": 6 },
|
||||
{ "country": "Romania", "count": 9 },
|
||||
{ "country": "Russia", "count": 6 },
|
||||
{ "country": "Saudi Arabia", "count": 12 },
|
||||
{ "country": "Scotland", "count": 2 },
|
||||
{ "country": "Serbia", "count": 3 },
|
||||
{ "country": "Singapore", "count": 49 },
|
||||
{ "country": "Slovakia", "count": 1 },
|
||||
{ "country": "South Korea", "count": 109 },
|
||||
{ "country": "Spain", "count": 12 },
|
||||
{ "country": "Sweden", "count": 11 },
|
||||
{ "country": "Switzerland", "count": 19 },
|
||||
{ "country": "Taiwan", "count": 112 },
|
||||
{ "country": "Thailand", "count": 38 },
|
||||
{ "country": "Turkey", "count": 11 },
|
||||
{ "country": "United Arab Emirates", "count": 67 },
|
||||
{ "country": "United Kingdom", "count": 76 },
|
||||
{ "country": "United States", "count": 55 },
|
||||
{ "country": "Universal", "count": 2 },
|
||||
{ "country": "Vietnam", "count": 2 }
|
||||
]
|
||||
},
|
||||
"scholarships_page_1": [
|
||||
{
|
||||
"name": "Boustany Foundation Scholarship, MBA - Harvard University",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S2"],
|
||||
"country": "United States",
|
||||
"open_date": "2026-05-21",
|
||||
"deadline": "2027-05-31",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/boustany-foundation-scholarship-mba-harvard-university-1"
|
||||
},
|
||||
{
|
||||
"name": "La Trobe University - La Trobe High Achiever Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S1"],
|
||||
"country": "Australia",
|
||||
"open_date": "2023-09-29",
|
||||
"deadline": "2026-12-31",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/la-trobe-university-la-trobe-high-achiever-scholarship-2"
|
||||
},
|
||||
{
|
||||
"name": "Charles Sturt University - International Student Support Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S1"],
|
||||
"country": "Australia",
|
||||
"open_date": "2026-05-21",
|
||||
"deadline": "2026-12-31",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/charles-sturt-university-international-student-support-scholarship-3"
|
||||
},
|
||||
{
|
||||
"name": "La Trobe University - La Trobe High Achiever Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S2"],
|
||||
"country": "Australia",
|
||||
"open_date": "2023-09-29",
|
||||
"deadline": "2026-12-31",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/la-trobe-university-la-trobe-high-achiever-scholarship-4"
|
||||
},
|
||||
{
|
||||
"name": "ETH Zurich - Excellence Scholarship & Opportunity Programme (ESOP)",
|
||||
"funding_type": "Fully Funded",
|
||||
"level": ["S2"],
|
||||
"country": "Switzerland",
|
||||
"open_date": "2026-11-01",
|
||||
"deadline": "2026-11-30",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/eth-zurich-excellence-scholarship-opportunity-programme-esop-5"
|
||||
},
|
||||
{
|
||||
"name": "Southern Taiwan University of Science and Technology - The STUST Scholarship for International Students",
|
||||
"funding_type": "Fully Funded",
|
||||
"level": ["S3"],
|
||||
"country": "Taiwan",
|
||||
"open_date": "2026-09-15",
|
||||
"deadline": "2026-11-30",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/southern-taiwan-university-of-science-and-technology-the-stust-scholarship-for-international-students-6"
|
||||
},
|
||||
{
|
||||
"name": "Southern Taiwan University of Science and Technology - The STUST Scholarship for International Students",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S2"],
|
||||
"country": "Taiwan",
|
||||
"open_date": "2026-09-15",
|
||||
"deadline": "2026-11-30",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/southern-taiwan-university-of-science-and-technology-the-stust-scholarship-for-international-students-7"
|
||||
},
|
||||
{
|
||||
"name": "University of Melbourne - Melbourne International Undergraduate Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S1"],
|
||||
"country": "Australia",
|
||||
"open_date": "2026-05-21",
|
||||
"deadline": "2026-10-31",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/university-of-melbourne-melbourne-international-undergraduate-scholarship-8"
|
||||
},
|
||||
{
|
||||
"name": "Charles Darwin University - CDU Global Merit Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S1"],
|
||||
"country": "Australia",
|
||||
"open_date": "2026-05-21",
|
||||
"deadline": "2026-10-09",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/charles-darwin-university-cdu-global-merit-scholarship-9"
|
||||
},
|
||||
{
|
||||
"name": "Deakin University - Vice-Chancellor's International Scholarship",
|
||||
"funding_type": "Partially Funded",
|
||||
"level": ["S1", "S2"],
|
||||
"country": "Australia",
|
||||
"open_date": "2026-05-21",
|
||||
"deadline": "2026-09-25",
|
||||
"status": "Pendaftaran dibuka",
|
||||
"url": "https://www.schoters.com/id/beasiswa/deakin-university-vice-chancellor-s-international-scholarship-10"
|
||||
},
|
||||
{
|
||||
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 153
|
||||
},
|
||||
"contact": {
|
||||
"email": "ask@schoters.com",
|
||||
"phone": "0811 9424 966",
|
||||
"address": "Gedung Sinarmas MSIG Tower Lt. 33, Jl. Jenderal Sudirman Kav. 21, Karet Kuningan, Setiabudi, Jakarta Selatan, Jakarta 12920"
|
||||
},
|
||||
"social_media": {
|
||||
"instagram": "https://www.instagram.com/schoters/",
|
||||
"facebook": "https://www.facebook.com/schoters",
|
||||
"youtube": "https://www.youtube.com/@Schoters",
|
||||
"tiktok": "https://www.tiktok.com/@schoters.id",
|
||||
"twitter_x": "https://x.com/schoters_com",
|
||||
"linkedin": "https://id.linkedin.com/company/schoters"
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
# Panduan Pengelolaan Data Kelompok C4A
|
||||
|
||||
Dokumentasi ini dibuat untuk memudahkan pengembang atau pengguna berikutnya dalam mengelola data Staf Pendukung, SOP, dan FAQ pada halaman C4A.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Aturan Umum Pengelolaan Data
|
||||
|
||||
1. **Lokasi Berkas Data**:
|
||||
Seluruh data disimpan dalam format JSON di direktori `groups/C4A/data/`:
|
||||
- Data Staf: `groups/C4A/data/data-staff.json`
|
||||
- Data SOP: `groups/C4A/data/data-sop.json`
|
||||
- Data FAQ: `groups/C4A/data/data-faq.json`
|
||||
|
||||
2. **Dukungan Bilingual (ID/EN)**:
|
||||
Semua teks konten yang ditampilkan ke pengguna mendukung dua bahasa. Anda perlu menggunakan objek dengan properti `id` (Bahasa Indonesia) dan `en` (Bahasa Inggris) pada setiap field teks. Jika properti `en` tidak diisi atau data masih berupa *string* biasa, sistem akan otomatis menggunakan Bahasa Indonesia sebagai *fallback*.
|
||||
|
||||
3. **Ikon FontAwesome (v4)**:
|
||||
Jika ingin mengubah atau menambahkan logo/ikon pada data SOP, harap gunakan daftar ikon dari **FontAwesome v4** melalui tautan berikut:
|
||||
👉 **[FontAwesome v4 Icons](https://fontawesome.com/v4/icons/)** (Contoh penggunaan: `fa-dot-circle-o`, `fa-graduation-cap`, `fa-flask`).
|
||||
|
||||
4. **Field Opsional**:
|
||||
Pada data SOP, properti **`staff_responsible`** (Staf Bertanggung Jawab) dan **`additional_info`** (Informasi Tambahan) bersifat **opsional**. Jika informasi tersebut tidak tersedia, kosongkan nilainya atau hapus propertinya dari JSON agar UI secara otomatis menyembunyikan bloknya.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Template Penambahan Data
|
||||
|
||||
Silakan salin-tempel (copy-paste) format di bawah ini ke dalam array JSON yang sesuai di folder `data/` untuk menambahkan data baru.
|
||||
|
||||
### 1. Template Data Staf (`data-staff.json`)
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Nama Staf Beserta Gelar",
|
||||
"role": {
|
||||
"id": "Peran / Jabatan Staf",
|
||||
"en": "Staff Role / Title"
|
||||
},
|
||||
"image": "assets/images/nama-file-foto.png",
|
||||
"location": {
|
||||
"id": "Lokasi Ruang Kerja / Kantor Staf",
|
||||
"en": "Staff Workspace / Office Location"
|
||||
},
|
||||
"contact_consent": true,
|
||||
"phone": "628xxxxxxxxxx",
|
||||
"schedule": [
|
||||
{
|
||||
"day": {
|
||||
"id": "Hari Kerja (contoh: Senin – Jumat)",
|
||||
"en": "Work Days (e.g., Monday – Friday)"
|
||||
},
|
||||
"time": "07.30–16.00 WIB",
|
||||
"break": {
|
||||
"id": "Jam Istirahat (contoh: istirahat 12.00–13.00 WIB)",
|
||||
"en": "Break Time (e.g., break 12.00–13.00 WIB)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"id": "Tugas dan tanggung jawab utama 1",
|
||||
"en": "Main task and responsibility 1"
|
||||
},
|
||||
{
|
||||
"id": "Tugas dan tanggung jawab utama 2",
|
||||
"en": "Main task and responsibility 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
*Catatan: Letakkan file gambar/foto staf ke dalam direktori `groups/C4A/assets/images/` dan masukkan nama berkas fotonya pada properti `image` di atas. Untuk nomor telepon (`phone`), Anda dapat menggunakan format `08xxxxxxxxxx` atau `628xxxxxxxxxx` karena skrip JavaScript akan melakukan normalisasi secara otomatis.*
|
||||
|
||||
### 2. Template Data SOP (`data-sop.json`)
|
||||
```json
|
||||
{
|
||||
"tab_id": "id-unik-sop-tanpa-spasi",
|
||||
"tab_number": 9,
|
||||
"category": "akademik",
|
||||
"icon": "fa-file-text",
|
||||
"title": {
|
||||
"id": "Judul Lengkap SOP",
|
||||
"en": "Full SOP Title"
|
||||
},
|
||||
"description": {
|
||||
"id": "Deskripsi singkat mengenai SOP ini.",
|
||||
"en": "Brief description of this SOP."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "Tujuan dibuatnya standar operasional prosedur ini.",
|
||||
"en": "The purpose of creating this standard operating procedure."
|
||||
},
|
||||
"requirements": [
|
||||
{ "id": "Persyaratan administratif/akademik 1", "en": "Administrative/academic requirement 1" },
|
||||
{ "id": "Persyaratan administratif/akademik 2", "en": "Administrative/academic requirement 2" }
|
||||
],
|
||||
"procedures": [
|
||||
{ "id": "Langkah alur prosedur ke-1", "en": "Procedure flow step 1" },
|
||||
{ "id": "Langkah alur prosedur ke-2", "en": "Procedure flow step 2" }
|
||||
],
|
||||
"staff_responsible": [
|
||||
"👤 Staf Administrasi Informatika / Informatics Administrative Staff"
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Informasi tambahan penting.",
|
||||
"en": "Important additional information."
|
||||
}
|
||||
}
|
||||
```
|
||||
*Catatan: Properti `category` bernilai antara `"akademik"` atau `"praktikum"`. Properti `staff_responsible` dan `additional_info` bersifat opsional.*
|
||||
|
||||
### 3. Template Data FAQ (`data-faq.json`)
|
||||
```json
|
||||
{
|
||||
"id": 11,
|
||||
"question": {
|
||||
"id": "Pertanyaan yang sering diajukan?",
|
||||
"en": "Frequently asked question?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Jawaban dari pertanyaan tersebut. Boleh menggunakan tag HTML seperti <strong>tebal</strong>.",
|
||||
"en": "Answer to the question. HTML tags like <strong>bold</strong> can be used."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -1,112 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"question": {
|
||||
"id": "Apa saja syarat administratif agar saya bisa mengikuti perkuliahan?",
|
||||
"en": "What are the administrative requirements for me to attend lectures?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Syarat untuk mengikuti perkuliahan adalah mahasiswa telah melakukan <strong>pembayaran UKT</strong>, mengisi <strong>KRS</strong>, dan terdaftar di <strong>kelas Edlink</strong>.",
|
||||
"en": "The requirements to attend lectures are that the student has completed <strong>UKT payment</strong>, filled out the <strong>KRS</strong>, and is registered in <strong>Edlink classes</strong>."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"question": {
|
||||
"id": "Berapa persentase kehadiran minimal agar bisa mengikuti Ujian Akhir Semester (UAS)?",
|
||||
"en": "What is the minimum attendance percentage required to take the Final Semester Exam (UAS)?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Mahasiswa wajib memenuhi minimal <strong>75% kehadiran</strong> dari total pertemuan perkuliahan. Mahasiswa yang tidak memenuhi syarat kehadiran ini tidak diperkenankan mengikuti UAS.",
|
||||
"en": "Students must meet a minimum of <strong>75% attendance</strong> out of the total lecture sessions. Students who do not meet this attendance requirement are not permitted to take the UAS."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"question": {
|
||||
"id": "Apakah pengisian evaluasi perkuliahan itu wajib?",
|
||||
"en": "Is filling out the lecture evaluation mandatory?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Ya, karena pengisian evaluasi perkuliahan dapat menjadi salah satu syarat untuk <strong>mengakses hasil studi</strong> sesuai dengan kebijakan yang berlaku. Hasil ini sangat penting untuk perbaikan dan pengembangan kualitas pembelajaran prodi ke depannya.",
|
||||
"en": "Yes, because completing the lecture evaluation may be one of the requirements to <strong>access study results</strong> in accordance with applicable policies. This is very important for improving and developing the quality of the study program's learning in the future."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"question": {
|
||||
"id": "Berapa minimal SKS dan IPK untuk bisa mendaftar Kerja Praktik (KP)?",
|
||||
"en": "What is the minimum number of credits (SKS) and GPA required to register for an Internship (KP)?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Untuk mendaftar KP, mahasiswa harus sudah lulus minimal <strong>100 SKS</strong> dan memiliki Indeks Prestasi Kumulatif (IPK) <strong>≥ 2,00</strong>.",
|
||||
"en": "To register for KP, students must have passed at least <strong>100 credits (SKS)</strong> and have a Cumulative Grade Point Average (GPA) of <strong>≥ 2.00</strong>."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"question": {
|
||||
"id": "Apakah ada syarat kelulusan mata kuliah tertentu sebelum saya bisa mengajukan Kerja Praktik (KP)?",
|
||||
"en": "Are there specific course completion requirements before I can apply for an Internship (KP)?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Ya, selain memenuhi syarat minimal SKS dan IPK, mahasiswa juga diwajibkan telah lulus mata kuliah <strong>Penulisan Proposal Tugas Akhir dengan nilai minimal C</strong>. Selain itu, mahasiswa juga harus menyerahkan berkas pendukung pengajuan KP seperti KTM, riwayat studi, transkrip nilai, dan surat permohonan.",
|
||||
"en": "Yes, in addition to meeting the minimum SKS and GPA requirements, students are also required to have passed the <strong>Final Project Proposal Writing course with a minimum grade of C</strong>. Furthermore, students must also submit supporting documents for the KP application, such as student ID card, academic record, transcript, and application letter."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"question": {
|
||||
"id": "Apa kualifikasi yang dibutuhkan untuk Pembimbing Lapangan di instansi tempat KP?",
|
||||
"en": "What qualifications are required for a Field Supervisor at the KP institution?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Pembimbing Lapangan dari tempat pelaksanaan KP diwajibkan memiliki kualifikasi pendidikan minimal <strong>S1 atau D4</strong>.",
|
||||
"en": "The Field Supervisor from the KP institution is required to have a minimum educational qualification of <strong>Bachelor's (S1) or Applied Bachelor's (D4)</strong>."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"question": {
|
||||
"id": "Bolehkah saya mengambil mata kuliah Kerja Praktik (KP) bersamaan dengan Tugas Akhir?",
|
||||
"en": "Can I take the Internship (KP) course concurrently with my Final Project (Tugas Akhir)?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Boleh. Mata Kuliah Kerja Praktik (KP) dapat diambil secara bersamaan dengan <strong>Tugas Akhir 1 (TA1)</strong> dan <strong>Tugas Akhir 2 (TA2)</strong> dalam satu semester yang sama.",
|
||||
"en": "Yes. The Internship (KP) course can be taken concurrently with <strong>Final Project 1 (TA1)</strong> and <strong>Final Project 2 (TA2)</strong> within the same semester."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"question": {
|
||||
"id": "Apa saja syarat akademik utama sebelum mengajukan judul Tugas Akhir (Skripsi)?",
|
||||
"en": "What are the main academic requirements before submitting a Final Project (Thesis) title?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Syarat utamanya adalah mahasiswa harus telah lulus minimal <strong>120 SKS</strong>, memiliki <strong>IPK minimal 2,00</strong>, telah lulus mata kuliah <strong>Penulisan Proposal Tugas Akhir dengan nilai minimal C</strong>, serta telah mendaftarkan mata kuliah skripsi pada KRS.",
|
||||
"en": "The main requirements are that the student must have passed at least <strong>120 credits (SKS)</strong>, have a <strong>minimum GPA of 2.00</strong>, have passed the <strong>Final Project Proposal Writing course with a minimum grade of C</strong>, and have registered the thesis course in the KRS."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"question": {
|
||||
"id": "Siapa yang berhak mendaftar menjadi Asisten Praktikum (Aslab)?",
|
||||
"en": "Who is eligible to register as a Practicum Assistant (Aslab)?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Rekrutmen terbuka bagi <strong>mahasiswa aktif Program Studi Informatika UNTAN</strong> yang telah lulus mata kuliah terkait (dengan nilai minimal sesuai ketentuan Dosen Pengampu) dan berkomitmen mengikuti seluruh jadwal praktikum selama semester berjalan.",
|
||||
"en": "Recruitment is open to <strong>active students of the Informatics Study Program at UNTAN</strong> who have passed the relevant courses (with a minimum grade as per the Lecturer's requirements) and are committed to attending all practicum schedules throughout the semester."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"question": {
|
||||
"id": "Bagaimana prosedur meminjam Laboratorium Komputer untuk keperluan kegiatan kampus?",
|
||||
"en": "What is the procedure for borrowing the Computer Laboratory for campus activities?"
|
||||
},
|
||||
"answer": {
|
||||
"id": "Peminjam (dosen atau mahasiswa) harus mengajukan <strong>surat permohonan peminjaman</strong> kepada staf laboratorium pengelola dan menyampaikan jadwal penggunaannya. Penggunaan lab juga harus didampingi oleh <strong>dosen atau asisten praktikum</strong> yang bertanggung jawab.",
|
||||
"en": "The borrower (lecturer or student) must submit a <strong>borrowing request letter</strong> to the laboratory management staff and provide the usage schedule. Lab usage must also be supervised by a <strong>responsible lecturer or practicum assistant</strong>."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,515 +0,0 @@
|
||||
[
|
||||
{
|
||||
"tab_id": "kuliah",
|
||||
"tab_number": 1,
|
||||
"category": "akademik",
|
||||
"icon": "fa-chalkboard-teacher",
|
||||
"title": {
|
||||
"id": "SOP Pelaksanaan Kuliah",
|
||||
"en": "Lecture Procedure SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur tata cara dan tahapan pelaksanaan perkuliahan mulai dari persiapan hingga evaluasi pembelajaran.",
|
||||
"en": "Regulates the procedures and stages of lecture implementation from preparation to learning evaluation."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan untuk menjelaskan tata cara pelaksanaan perkuliahan bagi mahasiswa Program Studi Informatika Universitas Tanjungpura.",
|
||||
"en": "This SOP aims to explain the procedures for conducting lectures for students of the Informatics Study Program at Tanjungpura University."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Mahasiswa telah melakukan pembayaran UKT",
|
||||
"en": "Student has completed UKT payment"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa telah mengisi KRS",
|
||||
"en": "Student has filled out the KRS"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa terdaftar di kelas Edlink",
|
||||
"en": "Student is registered in Edlink classes"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa wajib mengikuti perkuliahan dan menyelesaikan tugas tepat waktu",
|
||||
"en": "Students must attend lectures and complete assignments on time"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa wajib mengisi daftar hadir dan memberikan alasan resmi jika tidak hadir",
|
||||
"en": "Students must fill in the attendance list and provide an official reason if absent"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa memenuhi minimal 70% kehadiran untuk mengikuti UAS",
|
||||
"en": "Students must meet a minimum of 70% attendance to take the UAS"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Dosen memasuki ruang perkuliahan sesuai jadwal yang telah ditetapkan",
|
||||
"en": "The lecturer enters the lecture room according to the set schedule"
|
||||
},
|
||||
{
|
||||
"id": "Dosen mengabsensi mahasiswa dengan mencantumkan keterangan hadir, izin, atau tidak hadir",
|
||||
"en": "The lecturer takes attendance, noting present, excused, or absent"
|
||||
},
|
||||
{
|
||||
"id": "Pada pertemuan pertama, dosen membuat kontrak perkuliahan secara tertulis bersama mahasiswa",
|
||||
"en": "At the first meeting, the lecturer creates a written lecture contract with students"
|
||||
},
|
||||
{
|
||||
"id": "Dosen menjelaskan materi perkuliahan sesuai RPS yang telah diserahkan ke program studi",
|
||||
"en": "The lecturer explains the course material according to the RPS submitted to the study program"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mendiskusikan, mencatat, dan mengkritisi materi perkuliahan",
|
||||
"en": "Students discuss, take notes, and critically engage with the lecture material"
|
||||
},
|
||||
{
|
||||
"id": "Dosen menyimpulkan materi perkuliahan dan memberikan penugasan",
|
||||
"en": "The lecturer summarizes the lecture material and gives assignments"
|
||||
}
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Mahasiswa yang tidak memenuhi persyaratan kehadiran minimal tidak diperkenankan mengikuti Ujian Akhir Semester (UAS) sesuai ketentuan akademik yang berlaku.",
|
||||
"en": "Students who do not meet the minimum attendance requirements are not permitted to take the Final Semester Exam (UAS) in accordance with applicable academic regulations."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "ujian",
|
||||
"tab_number": 2,
|
||||
"category": "akademik",
|
||||
"icon": "fa-file-signature",
|
||||
"title": {
|
||||
"id": "SOP Pelaksanaan Ujian (UTS & UAS)",
|
||||
"en": "Exam Procedure SOP (Mid-Term & Final Exams)"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur pedoman pelaksanaan Ujian Tengah Semester (UTS) dan Ujian Akhir Semester (UAS) mulai dari penentuan kriteria evaluasi, pelaksanaan, hingga perekapan nilai.",
|
||||
"en": "Regulates the guidelines for conducting Mid-Term Exams (UTS) and Final Semester Exams (UAS), from determining evaluation criteria and implementation to grade recording."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan untuk memastikan kelancaran proses evaluasi pembelajaran mahasiswa melalui UTS dan UAS agar berjalan tertib, terukur, dan sesuai dengan instruksi dosen pengampu mata kuliah.",
|
||||
"en": "This SOP aims to ensure the smooth process of student learning evaluation through UTS and UAS so that it runs orderly, measurably, and in accordance with the course lecturer's instructions."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Mahasiswa terdaftar aktif pada mata kuliah yang sedang berjalan pada semester tersebut",
|
||||
"en": "Students are actively registered in the courses running in that semester"
|
||||
},
|
||||
{
|
||||
"id": "Persentase kehadiran perkuliahan mahasiswa tidak kurang dari 75%",
|
||||
"en": "Student lecture attendance percentage is not less than 75%"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Dosen pengampu menentukan kriteria UTS dan UAS",
|
||||
"en": "The course lecturer determines the UTS and UAS criteria"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengikuti UTS atau UAS sesuai instruksi dosen pengampu, dan wajib melakukan presensi",
|
||||
"en": "Students take UTS or UAS according to the lecturer's instructions and must complete attendance"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengumpulkan hasil UTS atau UAS sesuai rentang waktu yang diberikan",
|
||||
"en": "Students submit UTS or UAS results within the given timeframe"
|
||||
},
|
||||
{
|
||||
"id": "Dosen menilai dan merekap pengerjaan UTS atau UAS mahasiswa",
|
||||
"en": "The lecturer grades and records the students' UTS or UAS work"
|
||||
}
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Segala bentuk kecurangan saat ujian, maupun tindakan plagiarisme dalam pengerjaan UTS/UAS, dapat mengakibatkan pembatalan nilai atau pemberian sanksi.",
|
||||
"en": "Any form of cheating during exams, as well as plagiarism in completing the UTS/UAS, may result in grade cancellation or sanctions."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "evaluasi",
|
||||
"tab_number": 3,
|
||||
"category": "akademik",
|
||||
"icon": "fa-chart-line",
|
||||
"title": {
|
||||
"id": "SOP Pelaksanaan Evaluasi Pengembangan Perkuliahan",
|
||||
"en": "Lecture Development Evaluation SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur tata cara pelaksanaan evaluasi perkuliahan oleh mahasiswa sebagai sarana umpan balik untuk meningkatkan kualitas proses belajar mengajar.",
|
||||
"en": "Regulates the procedures for conducting lecture evaluations by students as a feedback mechanism to improve the quality of the teaching and learning process."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan memberikan pedoman pelaksanaan evaluasi perkuliahan guna mendukung peningkatan mutu pembelajaran di Program Studi Informatika Universitas Tanjungpura.",
|
||||
"en": "This SOP aims to provide guidelines for conducting lecture evaluations to support the improvement of learning quality in the Informatics Study Program at Tanjungpura University."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Mahasiswa terdaftar aktif pada semester berjalan",
|
||||
"en": "Students are actively registered for the current semester"
|
||||
},
|
||||
{
|
||||
"id": "Telah mengikuti kegiatan perkuliahan pada mata kuliah yang dievaluasi",
|
||||
"en": "Has attended lecture activities for the course being evaluated"
|
||||
},
|
||||
{
|
||||
"id": "Tersedia akses ke sistem evaluasi perkuliahan",
|
||||
"en": "Access to the lecture evaluation system is available"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Program Studi membuka periode evaluasi perkuliahan",
|
||||
"en": "The Study Program opens the lecture evaluation period"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengakses satu untan dan mengisi kuesioner evaluasi perkuliahan",
|
||||
"en": "Students access the Satu Untan portal and fill out the lecture evaluation questionnaire"
|
||||
},
|
||||
{
|
||||
"id": "Sistem merekap hasil evaluasi yang telah diisi mahasiswa",
|
||||
"en": "The system records the evaluation results filled in by students"
|
||||
},
|
||||
{
|
||||
"id": "Program Studi meninjau hasil evaluasi sebagai bahan pengembangan perkuliahan",
|
||||
"en": "The Study Program reviews the evaluation results as material for lecture development"
|
||||
}
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Hasil evaluasi digunakan sebagai dasar perbaikan dan pengembangan kualitas pembelajaran pada semester berikutnya. Pengisian evaluasi perkuliahan dapat menjadi salah satu syarat untuk mengakses hasil studi sesuai kebijakan yang berlaku.",
|
||||
"en": "Evaluation results are used as a basis for improving and developing the quality of learning in the following semester. Completing the lecture evaluation may be one of the requirements for accessing study results in accordance with applicable policies."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "kp",
|
||||
"tab_number": 4,
|
||||
"category": "akademik",
|
||||
"icon": "fa-briefcase",
|
||||
"title": {
|
||||
"id": "SOP Kerja Praktik",
|
||||
"en": "Internship (KP) SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Panduan resmi pelaksanaan Kerja Praktek (KP) mulai dari tata cara pendaftaran, pelaksanaan, hingga seminar Kerja Praktek (KP).",
|
||||
"en": "The official guide for conducting the Internship (KP), from registration procedures and implementation to the KP seminar."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan memberikan pedoman kepada seluruh pihak terkait dalam pelaksanaan Kerja Praktek serta meningkatkan efisiensi dan efektivitas pelaksanaannya di Program Studi Informatika Fakultas Teknik Universitas Tanjungpura.",
|
||||
"en": "This SOP aims to provide guidelines to all relevant parties in the implementation of the Internship and to improve the efficiency and effectiveness of its execution in the Informatics Study Program, Faculty of Engineering, Tanjungpura University."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Telah lulus minimal 100 SKS",
|
||||
"en": "Has passed a minimum of 100 credits (SKS)"
|
||||
},
|
||||
{
|
||||
"id": "Memiliki IPK minimal 2,00",
|
||||
"en": "Has a minimum GPA of 2.00"
|
||||
},
|
||||
{
|
||||
"id": "Telah lulus mata kuliah Penulisan Proposal Tugas Akhir dengan nilai minimal C",
|
||||
"en": "Has passed the Final Project Proposal Writing course with a minimum grade of C"
|
||||
},
|
||||
{
|
||||
"id": "Menyerahkan berkas pendukung pengajuan KP (KTM, riwayat studi, transkrip, dan surat permohonan)",
|
||||
"en": "Submits supporting documents for the KP application (student ID, academic record, transcript, and application letter)"
|
||||
},
|
||||
{
|
||||
"id": "Pembimbing Lapangan dari tempat KP memiliki kualifikasi pendidikan minimal S1/D4",
|
||||
"en": "The Field Supervisor at the KP institution holds a minimum educational qualification of Bachelor's (S1) or Applied Bachelor's (D4)"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Mahasiswa mengusulkan topik, draf proposal, dan menyerahkan berkas KP",
|
||||
"en": "Students propose a topic, submit a draft proposal, and submit KP documents"
|
||||
},
|
||||
{
|
||||
"id": "Staf Administrasi memverifikasi kelengkapan berkas",
|
||||
"en": "Administrative Staff verifies the completeness of documents"
|
||||
},
|
||||
{
|
||||
"id": "Ketua Prodi menerbitkan surat pengantar KP",
|
||||
"en": "The Head of the Study Program issues the KP introductory letter"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melaksanakan KP dan mengikuti bimbingan",
|
||||
"en": "Students carry out the KP and attend supervision sessions"
|
||||
},
|
||||
{
|
||||
"id": "Pembimbing Lapangan memberikan penilaian KP",
|
||||
"en": "The Field Supervisor provides a KP assessment"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengajukan seminar dan Prodi menerbitkan undangan",
|
||||
"en": "Students apply for the seminar and the Study Program issues an invitation"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melaksanakan seminar KP",
|
||||
"en": "Students conduct the KP seminar"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melakukan revisi laporan akhir KP",
|
||||
"en": "Students revise the final KP report"
|
||||
}
|
||||
],
|
||||
"staff_responsible": [
|
||||
"👤 Staf Administrasi Informatika / Informatics Administrative Staff"
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Mata Kuliah Kerja Praktik (KP) dapat diambil bersamaan dengan Tugas Akhir 1 (TA1) dan Tugas Akhir 2 (TA2) dalam semester yang sama.",
|
||||
"en": "The Internship (KP) course can be taken concurrently with Final Project 1 (TA1) and Final Project 2 (TA2) in the same semester."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "ta",
|
||||
"tab_number": 5,
|
||||
"category": "akademik",
|
||||
"icon": "fa-graduation-cap",
|
||||
"title": {
|
||||
"id": "SOP Tugas Akhir/Skripsi",
|
||||
"en": "Final Project / Thesis SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur tata cara pelaksanaan skripsi mulai dari pengajuan judul hingga ujian dan penyelesaian skripsi mahasiswa.",
|
||||
"en": "Regulates the procedures for thesis implementation, from title submission to examination and completion of the student's thesis."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan untuk memberikan pedoman kepada mahasiswa dan dosen dalam pelaksanaan skripsi agar proses penyusunan skripsi berjalan tertib, terarah, dan sesuai ketentuan akademik.",
|
||||
"en": "This SOP aims to provide guidance to students and lecturers in the implementation of the thesis so that the thesis writing process runs in an orderly, directed, and academically compliant manner."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Telah lulus minimal 120 SKS",
|
||||
"en": "Has passed a minimum of 120 credits (SKS)"
|
||||
},
|
||||
{
|
||||
"id": "Memiliki IPK minimal 2,00",
|
||||
"en": "Has a minimum GPA of 2.00"
|
||||
},
|
||||
{
|
||||
"id": "Telah lulus mata kuliah Penulisan Proposal Tugas Akhir dengan nilai minimal C",
|
||||
"en": "Has passed the Final Project Proposal Writing course with a minimum grade of C"
|
||||
},
|
||||
{
|
||||
"id": "Telah mendaftarkan mata kuliah skripsi pada KRS",
|
||||
"en": "Has registered the thesis course in the KRS"
|
||||
},
|
||||
{
|
||||
"id": "Telah menyiapkan judul dan proposal skripsi",
|
||||
"en": "Has prepared a thesis title and proposal"
|
||||
},
|
||||
{
|
||||
"id": "Memiliki dosen pembimbing skripsi",
|
||||
"en": "Has a thesis supervisor"
|
||||
},
|
||||
{
|
||||
"id": "Melengkapi berkas persyaratan",
|
||||
"en": "Completes the required documents"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Mahasiswa mengajukan judul dan proposal skripsi",
|
||||
"en": "Students submit the thesis title and proposal"
|
||||
},
|
||||
{
|
||||
"id": "Program studi menetapkan dosen pembimbing skripsi",
|
||||
"en": "The study program assigns a thesis supervisor"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melakukan bimbingan dan penyusunan skripsi",
|
||||
"en": "Students conduct supervision sessions and write the thesis"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengajukan seminar atau sidang skripsi",
|
||||
"en": "Students apply for the thesis seminar or defense"
|
||||
},
|
||||
{
|
||||
"id": "Tim penguji dan pembimbing melaksanakan seminar skripsi",
|
||||
"en": "The examination committee and supervisor conduct the thesis seminar"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melakukan revisi skripsi",
|
||||
"en": "Students revise the thesis"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa menyerahkan skripsi final",
|
||||
"en": "Students submit the final thesis"
|
||||
}
|
||||
],
|
||||
"staff_responsible": [
|
||||
"👤 Staf Administrasi Informatika / Informatics Administrative Staff"
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Mahasiswa diberi waktu maksimal 2 semester untuk menyelesaikan Tugas Akhir. Perpanjangan dapat dilakukan dengan persetujuan tertulis dari Kaprodi dan Pembimbing.",
|
||||
"en": "Students are given a maximum of 2 semesters to complete the Final Project. Extensions can be made with written approval from the Head of Study Program and the Supervisor."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "praktikum",
|
||||
"tab_number": 6,
|
||||
"category": "praktikum",
|
||||
"icon": "fa-flask",
|
||||
"title": {
|
||||
"id": "SOP Praktikum Mata Kuliah",
|
||||
"en": "Course Practicum SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur tata cara pelaksanaan praktikum mata kuliah agar kegiatan praktikum berjalan tertib, aman, dan sesuai ketentuan pembelajaran.",
|
||||
"en": "Regulates the procedures for conducting course practicums so that practicum activities run in an orderly, safe, and learning-compliant manner."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan untuk menjelaskan tata cara pelaksanaan perkuliahan yang memiliki komponen praktikum pada semester berjalan.",
|
||||
"en": "This SOP aims to explain the procedures for conducting courses that have a practicum component in the current semester."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Terdaftar sebagai peserta mata kuliah yang memiliki komponen praktikum pada semester berjalan",
|
||||
"en": "Registered as a participant in courses with a practicum component in the current semester"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa membawa perlengkapan praktikum yang diperlukan",
|
||||
"en": "Students bring the required practicum equipment"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Dosen mata kuliah menuntaskan pembahasan materi yang menjadi dasar praktikum",
|
||||
"en": "The course lecturer completes the discussion of material that forms the basis of the practicum"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mengisi daftar hadir dan menerima penjelasan prosedur praktikum",
|
||||
"en": "Students fill in the attendance list and receive an explanation of the practicum procedures"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa melaksanakan praktikum sesuai modul di bawah pengawasan dosen atau asisten praktikum",
|
||||
"en": "Students carry out the practicum according to the module under the supervision of the lecturer or practicum assistant"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa menyusun dan mengumpulkan laporan praktikum sesuai tenggat waktu",
|
||||
"en": "Students compile and submit the practicum report by the deadline"
|
||||
},
|
||||
{
|
||||
"id": "Dosen atau asisten melakukan penilaian laporan dan hasil praktikum sebagai komponen nilai akhir",
|
||||
"en": "The lecturer or assistant grades the practicum report and results as a component of the final grade"
|
||||
}
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Mahasiswa wajib mengikuti seluruh ketentuan praktikum dan menjaga ketertiban serta keamanan selama kegiatan praktikum berlangsung.",
|
||||
"en": "Students must comply with all practicum regulations and maintain order and safety throughout the practicum activities."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "rekrut_asprak",
|
||||
"tab_number": 7,
|
||||
"category": "praktikum",
|
||||
"icon": "fa-user-plus",
|
||||
"title": {
|
||||
"id": "SOP Perekrutan Asisten Praktikum",
|
||||
"en": "Practicum Assistant Recruitment SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Prosedur perekrutan Asisten Praktikum (Aslab) mulai dari pendataan kebutuhan mata kuliah, proses seleksi, hingga pengumuman kelulusan.",
|
||||
"en": "The procedure for recruiting Practicum Assistants (Aslab), from identifying course needs and the selection process to announcing results."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan sebagai panduan baku untuk menyelenggarakan rekrutmen Asisten Praktikum secara transparan, guna menyaring mahasiswa Informatika UNTAN yang kompeten dan berkomitmen sesuai kualifikasi mata kuliah.",
|
||||
"en": "This SOP aims to serve as a standard guide for conducting transparent Practicum Assistant recruitment, in order to select competent and committed Informatics UNTAN students according to course qualifications."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Mahasiswa aktif Program Studi Informatika UNTAN",
|
||||
"en": "Active student of the Informatics Study Program at UNTAN"
|
||||
},
|
||||
{
|
||||
"id": "Lulus mata kuliah terkait dengan nilai minimal ketentuan Dosen Pengampu",
|
||||
"en": "Passed the relevant course with a minimum grade as per the Lecturer's requirements"
|
||||
},
|
||||
{
|
||||
"id": "Berkomitmen mengikuti seluruh jadwal dan kontrak praktikum selama semester berjalan",
|
||||
"en": "Committed to following all practicum schedules and contracts throughout the current semester"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Dosen mendata kebutuhan kuota dan kualifikasi Asisten Praktikum",
|
||||
"en": "The lecturer records the quota needs and qualifications for Practicum Assistants"
|
||||
},
|
||||
{
|
||||
"id": "Dosen mengumumkan pembukaan rekrutmen asisten praktikum",
|
||||
"en": "The lecturer announces the opening of Practicum Assistant recruitment"
|
||||
},
|
||||
{
|
||||
"id": "Mahasiswa mendaftarkan diri sesuai persyaratan yang diberikan",
|
||||
"en": "Students register according to the given requirements"
|
||||
},
|
||||
{
|
||||
"id": "Dosen melakukan seleksi wawancara kepada calon asisten praktikum",
|
||||
"en": "The lecturer conducts interview selection for practicum assistant candidates"
|
||||
},
|
||||
{
|
||||
"id": "Dosen mengumumkan calon yang diterima berdasarkan hasil seleksi",
|
||||
"en": "The lecturer announces the accepted candidates based on the selection results"
|
||||
}
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Keputusan hasil seleksi bersifat final dan ditetapkan oleh dosen pengampu mata kuliah. Asisten Praktikum yang terpilih wajib mematuhi seluruh ketentuan yang berlaku selama menjalankan tugas sebagai Asisten Praktikum.",
|
||||
"en": "The selection results are final and determined by the course lecturer. Selected Practicum Assistants must comply with all applicable regulations while performing their duties as Practicum Assistants."
|
||||
}
|
||||
},
|
||||
{
|
||||
"tab_id": "labkom",
|
||||
"tab_number": 8,
|
||||
"category": "praktikum",
|
||||
"icon": "fa-desktop",
|
||||
"title": {
|
||||
"id": "SOP Peminjaman Laboratorium Komputer",
|
||||
"en": "Computer Laboratory Borrowing SOP"
|
||||
},
|
||||
"description": {
|
||||
"id": "Mengatur tata cara peminjaman dan penggunaan laboratorium komputer untuk mendukung kegiatan praktikum dan pembelajaran.",
|
||||
"en": "Regulates the procedures for borrowing and using the computer laboratory to support practicum activities and learning."
|
||||
},
|
||||
"purpose": {
|
||||
"id": "SOP ini bertujuan menjelaskan tata cara peminjaman lab komputer dan memastikan peminjaman mendukung kelancaran proses belajar mengajar di Program Studi Informatika Universitas Tanjungpura.",
|
||||
"en": "This SOP aims to explain the procedures for borrowing the computer lab and to ensure that borrowing supports the smooth teaching and learning process in the Informatics Study Program at Tanjungpura University."
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "Melampirkan surat permohonan peminjaman laboratorium",
|
||||
"en": "Attach a laboratory borrowing request letter"
|
||||
},
|
||||
{
|
||||
"id": "Terdapat dosen atau asisten praktikum yang bertanggung jawab",
|
||||
"en": "A responsible lecturer or practicum assistant is present"
|
||||
},
|
||||
{
|
||||
"id": "Pengguna menaati tata tertib dan ketentuan penggunaan perangkat laboratorium",
|
||||
"en": "Users comply with the rules and regulations for using laboratory equipment"
|
||||
}
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"id": "Dosen atau mahasiswa mengajukan surat permohonan peminjaman laboratorium kepada pengelola",
|
||||
"en": "The lecturer or student submits a laboratory borrowing request letter to the management"
|
||||
},
|
||||
{
|
||||
"id": "Pengguna menyampaikan jadwal penggunaan laboratorium",
|
||||
"en": "The user submits the laboratory usage schedule"
|
||||
},
|
||||
{
|
||||
"id": "Pengelola menyusun jadwal penggunaan laboratorium",
|
||||
"en": "The management arranges the laboratory usage schedule"
|
||||
},
|
||||
{
|
||||
"id": "Laboratorium digunakan sesuai jadwal dan ketentuan yang berlaku",
|
||||
"en": "The laboratory is used according to the schedule and applicable regulations"
|
||||
}
|
||||
],
|
||||
"staff_responsible": [
|
||||
"👤 Staf Laboratorium Informatika / Informatics Laboratory Staff"
|
||||
],
|
||||
"additional_info": {
|
||||
"id": "Pengguna laboratorium wajib menjaga kebersihan, keamanan, dan kondisi perangkat laboratorium selama kegiatan berlangsung.",
|
||||
"en": "Laboratory users must maintain the cleanliness, safety, and condition of laboratory equipment throughout the activity."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,82 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Tuning Rahayu, S.Hut.",
|
||||
"role": {
|
||||
"id": "Staf Administrasi Informatika",
|
||||
"en": "Informatics Administrative Staff"
|
||||
},
|
||||
"image": "assets/images/foto-profile.jpeg",
|
||||
"location": {
|
||||
"id": "Ruang Administrasi Prodi Informatika",
|
||||
"en": "Informatics Study Program Administration Room"
|
||||
},
|
||||
"contact_consent": true,
|
||||
"phone": "6282350250892",
|
||||
"schedule": [
|
||||
{
|
||||
"day": {
|
||||
"id": "Senin – Jumat",
|
||||
"en": "Monday – Friday"
|
||||
},
|
||||
"time": "07.30–16.00 WIB",
|
||||
"break": {
|
||||
"id": "istirahat 12.00–13.00 WIB",
|
||||
"en": "break 12.00–13.00 WIB"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"id": "Administrasi mahasiswa",
|
||||
"en": "Student administration"
|
||||
},
|
||||
{
|
||||
"id": "Informasi berkas akademik",
|
||||
"en": "Academic document information"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Mahyus Safutra, A.Md.",
|
||||
"role": {
|
||||
"id": "Staf Laboratorium Informatika",
|
||||
"en": "Informatics Laboratory Staff"
|
||||
},
|
||||
"image": "assets/images/foto-profile.jpeg",
|
||||
"location": {
|
||||
"id": "Laboratorium Informatika",
|
||||
"en": "Informatics Laboratory"
|
||||
},
|
||||
"contact_consent": true,
|
||||
"phone": "895338672533",
|
||||
"schedule": [
|
||||
{
|
||||
"day": {
|
||||
"id": "Senin – Jumat",
|
||||
"en": "Monday – Friday"
|
||||
},
|
||||
"time": "07.30–16.00 WIB",
|
||||
"break": {
|
||||
"id": "istirahat 12.00–13.00 WIB",
|
||||
"en": "break 12.00–13.00 WIB"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"id": "Pengelolaan laboratorium",
|
||||
"en": "Laboratory management"
|
||||
},
|
||||
{
|
||||
"id": "Dukungan praktikum",
|
||||
"en": "Practicum support"
|
||||
},
|
||||
{
|
||||
"id": "Peminjaman ruangan dan alat",
|
||||
"en": "Room and equipment borrowing"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id" class="scroll-smooth">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -8,12 +7,9 @@
|
||||
<link rel="stylesheet" href="../../assets/global/output.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:ital,wght@0,100..900;1,100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<style>
|
||||
<style>
|
||||
/* ===== MOBILE NAV STABILITY FIX =====
|
||||
Prevents logo bar from collapsing/shifting on Android when
|
||||
address bar hides/shows and triggers viewport reflow.
|
||||
@@ -297,10 +293,9 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="text-slate-800 antialiased bg-white selection:bg-untan-yellow selection:text-black">
|
||||
|
||||
<a href="#main-content" class="skip-link">Langsung ke Konten Utama</a>
|
||||
<a href="#main-content" class="skip-link" data-i18n="skip_to_content">Langsung ke Konten Utama</a>
|
||||
|
||||
<div id="nav-wrapper"
|
||||
class="fixed top-0 left-0 right-0 z-50 transition-shadow duration-300 shadow-sm border-b border-slate-200 will-change-auto">
|
||||
@@ -378,7 +373,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="tentang">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>Tentang Kami</span>
|
||||
<span data-i18n="nav_about">Tentang Kami</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -387,7 +382,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="program">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>Program</span>
|
||||
<span data-i18n="nav_program">Program</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -396,7 +391,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="sdm">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>SDM</span>
|
||||
<span data-i18n="nav_sdm">SDM</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -405,7 +400,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="fasilitas">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>Fasilitas</span>
|
||||
<span data-i18n="nav_facilities">Fasilitas</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -414,7 +409,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="karya">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>Karya & Agenda</span>
|
||||
<span data-i18n="nav_works">Karya & Agenda</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -423,7 +418,7 @@
|
||||
<div class="nav-menu-item relative" data-menu="kontak">
|
||||
<button
|
||||
class="nav-menu-btn px-4 py-2.5 text-sm font-bold text-slate-800 hover:text-untan-blue rounded-sm transition-all flex items-center gap-1.5 uppercase tracking-wide">
|
||||
<span>Kontak</span>
|
||||
<span data-i18n="nav_contact">Kontak</span>
|
||||
<i class="fas fa-chevron-down text-[10px] opacity-70 transition-transform duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1144,149 +1139,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<main id="main-content" class="pt-[88px] lg:pt-[160px] min-h-screen bg-white">
|
||||
<div class="container mx-auto px-4 lg:px-8 py-12">
|
||||
<div class="mb-8 pb-8 border-b border-slate-200">
|
||||
<span
|
||||
class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas
|
||||
Halaman Statis</span>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Staf Pendukung dan Standar Operasional
|
||||
Prosedur (SOP)</h1>
|
||||
<p class="text-slate-500 text-sm">Kelompok 04 — C4A · Informatika UNTAN 2026
|
||||
</p>
|
||||
<span class="bg-untan-yellow text-slate-900 text-[10px] font-bold uppercase tracking-widest px-2 py-0.5 inline-block mb-3">Tugas Halaman Statis</span>
|
||||
<h1 class="text-3xl lg:text-4xl font-bold text-slate-900 mb-2">Judul Halaman Group 04</h1>
|
||||
<p class="text-slate-500 text-sm">Kelompok 04 — C4A · Informatika UNTAN 2026</p>
|
||||
</div>
|
||||
<div id="assignment-body" class="prose prose-slate max-w-none">
|
||||
<!-- ================= START: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
<!-- HERO SECTION -->
|
||||
<section class="c4a-hero reveal">
|
||||
<div class="c4a-hero-inner">
|
||||
<div class="c4a-hero-tag" data-i18n="grp_c4a_hero_tag">LAYANAN AKADEMIK • INFORMATIKA UNTAN
|
||||
</div>
|
||||
<h1 class="c4a-hero-title" data-i18n-html="grp_c4a_hero_title">
|
||||
Staf Pendukung & <br/>
|
||||
<span class="highlight-orange">Standar Operasional Prosedur (SOP)</span>
|
||||
</h1>
|
||||
<p class="c4a-hero-desc" data-i18n="grp_c4a_hero_desc">Halaman ini menampilkan informasi petugas
|
||||
layanan akademik serta
|
||||
prosedur administrasi
|
||||
mahasiswa
|
||||
Program Studi Informatika UNTAN.</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="stat-staf">0</div>
|
||||
<div class="stat-label" data-i18n="grp_c4a_stat_staf">Staf Aktif</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="stat-sop">0</div>
|
||||
<div class="stat-label" data-i18n="grp_c4a_stat_sop">Jenis SOP</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- STAFF SECTION -->
|
||||
<section class="content-section">
|
||||
<div class="section-header">
|
||||
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_staff_tag">Tim
|
||||
Kami</span></div>
|
||||
<h2 class="section-title" data-i18n="grp_c4a_staff_title">Staf Pendukung</h2>
|
||||
<p class="section-subtitle" data-i18n="grp_c4a_staff_subtitle">Staf yang siap melayani kebutuhan
|
||||
akademik dan operasional
|
||||
laboratorium di Prodi Informatika UNTAN.</p>
|
||||
</div>
|
||||
<div class="staff-grid" id="staff-container">
|
||||
<!-- Kartu staf akan diisi secara dinamis melalui main.js berdasarkan data-staff.json -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SOP SECTION -->
|
||||
<section class="sop-section">
|
||||
<div class="section-header">
|
||||
<div class="section-tag"><span class="dot"></span> <span data-i18n="grp_c4a_sop_tag">Prosedur
|
||||
Resmi</span></div>
|
||||
<h2 class="section-title" data-i18n="grp_c4a_sop_title">Standar Operasional Prosedur (SOP)</h2>
|
||||
<p class="section-subtitle" data-i18n="grp_c4a_sop_subtitle">Standar Operasional Prosedur untuk
|
||||
berbagai kebutuhan administrasi
|
||||
akademik mahasiswa. Gunakan pencarian atau filter untuk menemukan SOP yang Anda butuhkan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH & FILTER BAR -->
|
||||
<div class="sop-toolbar reveal">
|
||||
<div class="sop-search-wrap">
|
||||
<i class="fas fa-search sop-search-icon"></i>
|
||||
<input type="search" id="sop-search-input" class="sop-search-input"
|
||||
data-i18n-placeholder="grp_c4a_sop_search_placeholder"
|
||||
placeholder="Cari SOP berdasarkan judul..." autocomplete="off" aria-label="Cari SOP" />
|
||||
<button class="sop-search-clear" id="sop-search-clear" aria-label="Hapus pencarian"
|
||||
style="display:none;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sop-filter-chips" id="sop-filter-chips" role="group"
|
||||
aria-label="Filter kategori SOP">
|
||||
<button class="sop-chip active" data-filter="semua" id="chip-semua"
|
||||
data-i18n="grp_c4a_sop_chip_semua">Semua</button>
|
||||
<button class="sop-chip" data-filter="akademik" id="chip-akademik">
|
||||
<i class="fas fa-graduation-cap"></i> <span
|
||||
data-i18n="grp_c4a_sop_chip_akademik">Akademik</span>
|
||||
</button>
|
||||
<button class="sop-chip" data-filter="praktikum" id="chip-praktikum">
|
||||
<i class="fas fa-flask"></i> <span
|
||||
data-i18n="grp_c4a_sop_chip_praktikum">Praktikum</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RESULT COUNT -->
|
||||
<div class="sop-result-info" id="sop-result-info" aria-live="polite"></div>
|
||||
|
||||
<!-- SOP CARD GRID -->
|
||||
<div class="sop-card-grid" id="sop-cards-container"></div>
|
||||
|
||||
<!-- EMPTY STATE -->
|
||||
<div class="sop-empty-state" id="sop-empty-state" style="display:none;">
|
||||
<i class="fas fa-search-minus sop-empty-icon"></i>
|
||||
<p class="sop-empty-title" data-i18n="grp_c4a_sop_empty_title">SOP tidak ditemukan</p>
|
||||
<p class="sop-empty-desc" data-i18n="grp_c4a_sop_empty_desc">Coba kata kunci lain atau reset
|
||||
filter pencarian.</p>
|
||||
<button class="sop-empty-reset" id="sop-empty-reset">
|
||||
<i class="fas fa-undo"></i> <span data-i18n="grp_c4a_sop_reset_btn">Reset Pencarian</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SOP DETAIL MODAL -->
|
||||
<div class="sop-modal-overlay" id="sop-modal-overlay" role="dialog" aria-modal="true"
|
||||
aria-labelledby="sop-modal-title">
|
||||
<div class="sop-modal-box" id="sop-modal-box">
|
||||
<button class="sop-modal-close" id="sop-modal-close" aria-label="Tutup detail SOP">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div id="sop-modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ SECTION -->
|
||||
<section class="faq-section-wrap">
|
||||
<div class="faq-section-inner">
|
||||
<div class="faq-header reveal">
|
||||
<div class="section-tag"><span class="dot"></span> <span
|
||||
data-i18n="grp_c4a_faq_tag">Pertanyaan Umum</span></div>
|
||||
<h2 data-i18n="grp_c4a_faq_title">Pertanyaan yang Sering Diajukan</h2>
|
||||
<p data-i18n="grp_c4a_faq_subtitle">Temukan jawaban cepat atas pertanyaan umum mahasiswa
|
||||
seputar layanan akademik prodi.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="faq-wrap" id="faq-container">
|
||||
<!-- Item FAQ diisi secara dinamis melalui main.js berdasarkan data-faq.json -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="module" src="./js/main.js"></script>
|
||||
<p>Tulis konten halaman kelompok di sini.</p>
|
||||
<h3>Subjudul</h3>
|
||||
<p>Gunakan paragraf, list, tabel, gambar, dan elemen konten lainnya.</p>
|
||||
<!-- ================= END: BAGIAN YANG DIEDIT KELOMPOK ================= -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1351,8 +1215,8 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full relative z-0 mt-4 pointer-events-none flex justify-center">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration"
|
||||
class="w-full h-auto object-cover object-bottom" style="object-position: bottom;">
|
||||
<img src="../../assets/global/footer.png" alt="Footer Illustration" class="w-full h-auto object-cover object-bottom"
|
||||
style="object-position: bottom;">
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1448,28 +1312,27 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn = document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function () {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function () {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const mobileBtn = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const closeMobileBtn= document.getElementById('close-mobile-menu');
|
||||
if (mobileBtn && mobileMenu) {
|
||||
mobileBtn.addEventListener('click', function() {
|
||||
const isOpen = !mobileMenu.classList.contains('-translate-x-full');
|
||||
mobileMenu.classList.toggle('-translate-x-full');
|
||||
mobileBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
|
||||
});
|
||||
}
|
||||
if (closeMobileBtn && mobileMenu) {
|
||||
closeMobileBtn.addEventListener('click', function() {
|
||||
mobileMenu.classList.add('-translate-x-full');
|
||||
if (mobileBtn) mobileBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
<script src="../../assets/global/localization.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Berisi fungsi-fungsi untuk mengambil data (Data Fetching).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mengambil data staf pendukung dari berkas JSON.
|
||||
* @returns {Promise<Array<Object>>} Daftar objek data staf
|
||||
*/
|
||||
export async function getStaffData() {
|
||||
try {
|
||||
const response = await fetch('data/data-staff.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data staf:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil data SOP dari berkas JSON.
|
||||
* @returns {Promise<Array<Object>>} Daftar objek data SOP
|
||||
*/
|
||||
export async function getSopData() {
|
||||
try {
|
||||
const response = await fetch('data/data-sop.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data SOP:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil data FAQ dari berkas JSON.
|
||||
* @returns {Promise<Array<Object>>} Daftar objek data FAQ
|
||||
*/
|
||||
export async function getFaqData() {
|
||||
try {
|
||||
const response = await fetch('data/data-faq.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Gagal memuat data FAQ:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Main Entry Point Kelompok C4A.
|
||||
* Menghubungkan pengambilan data dengan rendering UI dan interaksi halaman.
|
||||
*/
|
||||
|
||||
import { getStaffData, getSopData, getFaqData } from './data.js';
|
||||
import {
|
||||
renderStaffCards,
|
||||
renderSopCards,
|
||||
renderFaqSection,
|
||||
initSopInteraction,
|
||||
initAnimations,
|
||||
initFaq,
|
||||
applySopFilter
|
||||
} from './ui.js';
|
||||
|
||||
// Cache data secara global untuk mendukung rendering UI
|
||||
let cachedStaffData = [];
|
||||
let cachedSopData = [];
|
||||
let cachedFaqData = [];
|
||||
|
||||
/**
|
||||
* Menginisialisasi aplikasi dengan mengambil seluruh data secara pararel,
|
||||
* merender konten awal, serta mendaftarkan event listener interaksi UI.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function initApp() {
|
||||
try {
|
||||
const [staffData, sopData, faqData] = await Promise.all([
|
||||
getStaffData(),
|
||||
getSopData(),
|
||||
getFaqData()
|
||||
]);
|
||||
|
||||
cachedStaffData = staffData || [];
|
||||
cachedSopData = sopData || [];
|
||||
cachedFaqData = faqData || [];
|
||||
|
||||
renderStaffCards(cachedStaffData);
|
||||
renderSopCards(cachedSopData);
|
||||
renderFaqSection(cachedFaqData);
|
||||
|
||||
initSopInteraction();
|
||||
initFaq();
|
||||
|
||||
// Memberikan sedikit waktu tunda (jeda) agar rendering DOM selesai sebelum memulai animasi.
|
||||
setTimeout(() => {
|
||||
initAnimations();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('Terjadi kesalahan saat menginisialisasi halaman:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render UI saat bahasa berubah (data tetap sama, hanya label UI yang berganti via t())
|
||||
document.addEventListener('languageChanged', () => {
|
||||
renderStaffCards(cachedStaffData);
|
||||
renderSopCards(cachedSopData);
|
||||
renderFaqSection(cachedFaqData);
|
||||
initFaq();
|
||||
applySopFilter();
|
||||
|
||||
// Re-inisialisasi observer agar elemen .reveal yang baru di-render mendapat class .visible
|
||||
setTimeout(() => {
|
||||
initAnimations();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Jalankan inisialisasi aplikasi saat DOM siap
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
|
||||
@@ -1,681 +0,0 @@
|
||||
// --- Helper Fungsi ---
|
||||
|
||||
/**
|
||||
* Helper untuk menerjemahkan string berdasarkan key dari localization.js.
|
||||
* Menggunakan fallback jika key tidak ditemukan atau localization.js tidak aktif.
|
||||
*/
|
||||
function t(key, fallback) {
|
||||
if (window.t) {
|
||||
const val = window.t(key);
|
||||
if (val !== key) return val;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mendapatkan bahasa aktif dari localStorage atau default 'id'.
|
||||
* @returns {'id'|'en'} Kode bahasa aktif
|
||||
*/
|
||||
function getLang() {
|
||||
return localStorage.getItem('if_untan_lang') || 'id';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mengambil teks dari field bilingual JSON { id: '...', en: '...' }.
|
||||
* Jika field bukan objek (string lama / belum diupdate), kembalikan apa adanya.
|
||||
* @param {string|Object} field - Field data dari JSON
|
||||
* @returns {string} Teks sesuai bahasa aktif
|
||||
*/
|
||||
function loc(field) {
|
||||
if (!field) return '';
|
||||
if (typeof field === 'object' && field !== null) {
|
||||
const lang = getLang();
|
||||
return field[lang] || field['id'] || '';
|
||||
}
|
||||
return String(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisasi nomor telepon ke format standard e.g. 628...
|
||||
* @param {string} phone - Nomor telepon mentah
|
||||
* @returns {string} Nomor telepon ternormalisasi
|
||||
*/
|
||||
function normalizePhone(phone) {
|
||||
let digits = String(phone || '').replace(/\D/g, '');
|
||||
if (digits.startsWith('0')) {
|
||||
digits = `62${digits.slice(1)}`;
|
||||
} else if (digits.startsWith('8')) {
|
||||
digits = `62${digits}`;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format nomor telepon untuk tampilan UI
|
||||
* @param {string} phone - Nomor telepon mentah
|
||||
* @returns {string} Nomor telepon terformat (e.g. +62 8xx-xxxx-xxxx)
|
||||
*/
|
||||
function formatPhoneForDisplay(phone) {
|
||||
const norm = normalizePhone(phone);
|
||||
if (!norm) return '-';
|
||||
return `+${norm.slice(0, 2)} ${norm.slice(2, 5)}-${norm.slice(5, 9)}-${norm.slice(9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memvalidasi apakah nomor telepon valid untuk digunakan di WhatsApp.
|
||||
* @param {string} phone - Nomor telepon mentah
|
||||
* @returns {boolean} True jika nomor valid (seluler Indonesia 10-15 digit)
|
||||
*/
|
||||
function isValidWhatsAppNumber(phone) {
|
||||
const norm = normalizePhone(phone);
|
||||
return norm.startsWith('628') && norm.length >= 10 && norm.length <= 15;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enkripsi nomor telepon menggunakan Base64 untuk keamanan kontak.
|
||||
* @param {string} phone - Nomor telepon ternormalisasi
|
||||
* @returns {string} Nomor telepon terenkripsi Base64
|
||||
*/
|
||||
function encodePhone(phone) {
|
||||
return btoa(normalizePhone(phone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dekripsi nomor telepon dari Base64.
|
||||
* @param {string} encoded - Nomor telepon terenkripsi Base64
|
||||
* @returns {string} Nomor telepon terdekripsi
|
||||
*/
|
||||
function decodePhone(encoded) {
|
||||
try {
|
||||
return atob(encoded);
|
||||
} catch (error) {
|
||||
console.error('Gagal mendekripsi nomor telepon:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merender daftar staf pendukung ke dalam container HTML.
|
||||
* @param {Array<Object>} staffs - Array data staf pendukung
|
||||
*/
|
||||
export function renderStaffCards(staffs) {
|
||||
const container = document.getElementById('staff-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!staffs || staffs.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_staff', 'Data staf pendukung tidak tersedia saat ini.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
staffs.forEach((staff, index) => {
|
||||
const {
|
||||
name,
|
||||
role,
|
||||
image,
|
||||
location,
|
||||
phone,
|
||||
contact_consent: contactConsent,
|
||||
schedule,
|
||||
tasks
|
||||
} = staff;
|
||||
|
||||
const roleText = loc(role);
|
||||
const locationText = loc(location);
|
||||
|
||||
const scheduleHTML = schedule.map(sched =>
|
||||
`<div class="schedule-item">` +
|
||||
`<span class="s-day">${loc(sched.day)}</span>` +
|
||||
`<span class="s-time">${loc(sched.time)} <span class="s-break">| ${loc(sched.break)}</span></span>` +
|
||||
`</div>`
|
||||
).join('');
|
||||
|
||||
const tasksHTML = tasks.map(task => `<li>${loc(task)}</li>`).join('');
|
||||
const isPhoneValid = isValidWhatsAppNumber(phone);
|
||||
const encodedPhone = isPhoneValid ? encodePhone(phone) : '';
|
||||
const displayPhone = isPhoneValid ? formatPhoneForDisplay(phone) : '-';
|
||||
const hideContactItemClass = contactConsent ? '' : 'is-hidden';
|
||||
const hideContactActionClass = (contactConsent && isPhoneValid) ? '' : 'is-hidden';
|
||||
const initial = name ? name.charAt(0).toUpperCase() : '';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = `staff-card reveal reveal-delay-${index + 1}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="staff-card-header">
|
||||
<div class="staff-avatar">
|
||||
<span class="staff-initial">${initial}</span>
|
||||
<img src="${image}" alt="${name}" onerror="this.style.display='none'"/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="staff-name">${loc(name)}</div>
|
||||
<div class="staff-role">${roleText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="staff-card-body">
|
||||
<div class="staff-info-grid">
|
||||
<div class="staff-info-item"><span class="icon">📍</span><div><span class="label">${t('grp_c4a_label_location', 'Lokasi')}</span><span class="val">${locationText}</span></div></div>
|
||||
<div class="staff-info-item">
|
||||
<span class="icon">🕒</span>
|
||||
<div style="flex:1">
|
||||
<span class="label">${t('grp_c4a_label_service_hours', 'Jam Layanan')}</span>
|
||||
<div class="schedule-list">${scheduleHTML}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="staff-info-item staff-contact-item ${hideContactItemClass}"><span class="icon">📞</span><div><span class="label">${t('grp_c4a_label_wa_contact', 'Kontak WhatsApp')}</span><span class="val">${displayPhone}</span></div></div>
|
||||
</div>
|
||||
<div class="staff-divider"></div>
|
||||
<div class="staff-tasks-title">${t('grp_c4a_label_tasks', 'Tugas & Tanggung Jawab')}</div>
|
||||
<ul class="staff-tasks">${tasksHTML}</ul>
|
||||
</div>
|
||||
<div class="staff-card-actions staff-contact-actions ${hideContactActionClass}">
|
||||
<button class="btn-staff btn-staff-wa" data-wa="${encodedPhone}" aria-label="${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')} ${name}"><svg class="wa-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg> ${t('grp_c4a_label_send_wa', 'Kirim Pesan WA')}</button>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
|
||||
// Setup WA button click handler dengan dekrips nomor
|
||||
const waBtn = card.querySelector('[data-wa]');
|
||||
if (waBtn) {
|
||||
waBtn.addEventListener('click', () => {
|
||||
const phoneNum = decodePhone(waBtn.dataset.wa);
|
||||
if (phoneNum) {
|
||||
window.open(`https://wa.me/${phoneNum}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// ============================================================
|
||||
// --- Render SOP sebagai Card Grid dengan Search & Filter ---
|
||||
// ============================================================
|
||||
|
||||
const CATEGORY_COLORS = {
|
||||
akademik: 'sop-cat-akademik',
|
||||
praktikum: 'sop-cat-praktikum',
|
||||
};
|
||||
|
||||
/**
|
||||
* Ambil label kategori
|
||||
* @param {string} category - Kategori SOP ('akademik' atau 'praktikum')
|
||||
* @returns {string} Label kategori
|
||||
*/
|
||||
function getCatLabel(category) {
|
||||
const map = {
|
||||
akademik: t('grp_c4a_sop_chip_akademik', 'Akademik'),
|
||||
praktikum: t('grp_c4a_sop_chip_praktikum', 'Praktikum')
|
||||
};
|
||||
return map[category] || t('grp_c4a_sop_chip_akademik', 'Akademik');
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat HTML untuk satu SOP card di grid.
|
||||
* @param {Object} sop - Data objek SOP
|
||||
* @returns {string} String markup HTML kartu SOP
|
||||
*/
|
||||
function buildSopCard(sop) {
|
||||
const {
|
||||
tab_id: tabId,
|
||||
category,
|
||||
icon = 'fa-file-alt',
|
||||
title,
|
||||
description,
|
||||
procedures
|
||||
} = sop;
|
||||
|
||||
const titleText = loc(title);
|
||||
const descText = loc(description);
|
||||
|
||||
const catLabel = getCatLabel(category);
|
||||
const catClass = CATEGORY_COLORS[category] || '';
|
||||
|
||||
return `
|
||||
<article class="sop-grid-card reveal" data-tab-id="${tabId}" data-category="${category}" data-title="${titleText.toLowerCase()}">
|
||||
<div class="sgc-header">
|
||||
<div class="sgc-icon-wrap">
|
||||
<i class="fas ${icon}"></i>
|
||||
</div>
|
||||
<div class="sgc-meta">
|
||||
<h3 class="sgc-title">${titleText}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sgc-category-row">
|
||||
<span class="sgc-category ${catClass}">${catLabel}</span>
|
||||
</div>
|
||||
<p class="sgc-desc">${descText}</p>
|
||||
<div class="sgc-footer">
|
||||
<div class="sgc-steps">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
<span>${procedures.length} ${t('grp_c4a_label_steps', 'Langkah')}</span>
|
||||
</div>
|
||||
<button class="sgc-detail-btn" data-tab-id="${tabId}" aria-label="${t('grp_c4a_label_view_detail', 'Lihat Detail')} ${titleText}">
|
||||
${t('grp_c4a_label_view_detail', 'Lihat Detail')} <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat HTML untuk isi modal detail SOP.
|
||||
* @param {Object} sop - Data objek SOP
|
||||
* @returns {string} String markup HTML isi modal
|
||||
*/
|
||||
function buildSopModalContent(sop) {
|
||||
const {
|
||||
category,
|
||||
icon = 'fa-file-alt',
|
||||
title,
|
||||
description,
|
||||
purpose,
|
||||
requirements,
|
||||
procedures,
|
||||
staff_responsible: staffResponsible,
|
||||
additional_info: additionalInfo
|
||||
} = sop;
|
||||
|
||||
const titleText = loc(title);
|
||||
const descText = loc(description);
|
||||
const purposeText = loc(purpose);
|
||||
const additionalInfoText = loc(additionalInfo);
|
||||
|
||||
const catLabel = getCatLabel(category);
|
||||
const catClass = CATEGORY_COLORS[category] || '';
|
||||
const reqsHTML = requirements.map(req => `<li>${loc(req)}</li>`).join('');
|
||||
const procsHTML = procedures.map(proc => `<li>${loc(proc)}</li>`).join('');
|
||||
|
||||
// staffResponsible bersifat opsional — sembunyikan blok jika tidak ada
|
||||
const hasStaff = Array.isArray(staffResponsible) && staffResponsible.length > 0;
|
||||
const staffHTML = hasStaff
|
||||
? staffResponsible.map(staffName => `<span class="sop-staff-pill">${loc(staffName)}</span>`).join('')
|
||||
: '';
|
||||
const staffBlockHTML = hasStaff
|
||||
? `<div class="sop-staff-block">
|
||||
<div class="sop-block-label">${t('grp_c4a_label_responsible_staff', 'Staf Bertanggung Jawab (Bersifat Opsional)')}</div>
|
||||
<div class="sop-staff-pills">${staffHTML}</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// additionalInfo bersifat opsional — sembunyikan blok jika tidak ada
|
||||
const additionalInfoHTML = additionalInfoText
|
||||
? `<div class="sop-info-box">
|
||||
<span class="iico">ℹ️</span>
|
||||
<p class="sop-info-box-text"><strong>${t('grp_c4a_label_additional_info', 'Informasi Tambahan (Bersifat Opsional):')}</strong> ${additionalInfoText}</p>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="sop-modal-header">
|
||||
<div class="sop-modal-header-top">
|
||||
<div class="sop-modal-icon-wrap">
|
||||
<i class="fas ${icon}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sop-modal-badges">
|
||||
<span class="sgc-category ${catClass}">${catLabel}</span>
|
||||
</div>
|
||||
<h2 class="sop-modal-title" id="sop-modal-title">${titleText}</h2>
|
||||
<p class="sop-modal-desc">${descText}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sop-modal-body">
|
||||
<div class="sop-block">
|
||||
<div class="sop-block-label">${t('grp_c4a_label_purpose', 'Tujuan')}</div>
|
||||
<p class="sop-block-text">${purposeText}</p>
|
||||
</div>
|
||||
<div class="sop-two-col">
|
||||
<div>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_requirements', 'Persyaratan')}</div>
|
||||
<ul class="sop-list bullets">${reqsHTML}</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sop-block-label">${t('grp_c4a_label_procedure', 'Alur Prosedur')}</div>
|
||||
<ul class="sop-list numbered">${procsHTML}</ul>
|
||||
</div>
|
||||
</div>
|
||||
${staffBlockHTML}
|
||||
${additionalInfoHTML}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render semua SOP card ke grid.
|
||||
* @param {Array<Object>} sops - Array objek SOP
|
||||
*/
|
||||
export function renderSopCards(sops) {
|
||||
const container = document.getElementById('sop-cards-container');
|
||||
if (!container) return;
|
||||
|
||||
// simpan data global di element agar bisa diakses filter/search
|
||||
container._sopData = sops;
|
||||
|
||||
const emptyState = document.getElementById('sop-empty-state');
|
||||
|
||||
// Tampilkan pesan jika data SOP kosong atau gagal dimuat
|
||||
if (!sops || sops.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_sop', 'Data SOP tidak tersedia saat ini.')}</p>`;
|
||||
updateResultInfo(0, 0);
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sops.map(buildSopCard).join('');
|
||||
updateResultInfo(sops.length, sops.length);
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
}
|
||||
|
||||
/** Update teks info jumlah hasil */
|
||||
function updateResultInfo(shown, total) {
|
||||
const el = document.getElementById('sop-result-info');
|
||||
if (!el) return;
|
||||
if (shown === total) {
|
||||
const allTemplate = t('grp_c4a_sop_showing_all', 'Menampilkan {total} SOP');
|
||||
el.textContent = allTemplate.replace('{total}', total);
|
||||
} else {
|
||||
const filteredTemplate = t('grp_c4a_sop_showing_filtered', 'Menampilkan {shown} dari {total} SOP');
|
||||
el.textContent = filteredTemplate.replace('{shown}', shown).replace('{total}', total);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memfilter dan menyaring kartu SOP berdasarkan query pencarian dan chip filter aktif.
|
||||
*/
|
||||
export function applySopFilter() {
|
||||
const container = document.getElementById('sop-cards-container');
|
||||
if (!container) return;
|
||||
|
||||
const query = (document.getElementById('sop-search-input')?.value || '').toLowerCase().trim();
|
||||
const activeChip = document.querySelector('.sop-chip.active');
|
||||
const activeFilter = activeChip ? activeChip.dataset.filter : 'semua';
|
||||
|
||||
const cards = container.querySelectorAll('.sop-grid-card');
|
||||
let visibleCount = 0;
|
||||
|
||||
cards.forEach(card => {
|
||||
const title = card.dataset.title || '';
|
||||
const code = card.dataset.code || '';
|
||||
const category = card.dataset.category || '';
|
||||
|
||||
const matchesSearch = !query || title.includes(query) || code.includes(query);
|
||||
const matchesFilter = activeFilter === 'semua' || category === activeFilter;
|
||||
|
||||
const isVisible = matchesSearch && matchesFilter;
|
||||
card.style.display = isVisible ? '' : 'none';
|
||||
if (isVisible) visibleCount++;
|
||||
});
|
||||
|
||||
const emptyState = document.getElementById('sop-empty-state');
|
||||
if (emptyState) {
|
||||
// Tampilkan "SOP tidak ditemukan" HANYA jika memang ada data SOP tetapi hasil filter 0.
|
||||
// Jika data SOP memang kosong (tidak ada card sama sekali di data source), sembunyikan.
|
||||
const hasData = container._sopData && container._sopData.length > 0;
|
||||
emptyState.style.display = (hasData && visibleCount === 0) ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
updateResultInfo(visibleCount, cards.length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Membuka modal detail untuk SOP tertentu berdasarkan tab_id.
|
||||
* @param {string} tabId - ID unik tab/SOP yang ingin ditampilkan
|
||||
*/
|
||||
function openSopModal(tabId) {
|
||||
const container = document.getElementById('sop-cards-container');
|
||||
if (!container || !container._sopData) return;
|
||||
const sop = container._sopData.find(item => item.tab_id === tabId);
|
||||
if (!sop) return;
|
||||
|
||||
const modalContent = document.getElementById('sop-modal-content');
|
||||
const modalOverlay = document.getElementById('sop-modal-overlay');
|
||||
if (!modalContent || !modalOverlay) return;
|
||||
|
||||
modalContent.innerHTML = buildSopModalContent(sop);
|
||||
modalOverlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Gulir isi modal ke paling atas
|
||||
const modalBox = document.getElementById('sop-modal-box');
|
||||
if (modalBox) modalBox.scrollTop = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menutup modal detail SOP yang sedang aktif.
|
||||
*/
|
||||
function closeSopModal() {
|
||||
const modalOverlay = document.getElementById('sop-modal-overlay');
|
||||
if (modalOverlay) modalOverlay.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Menginisialisasi interaksi halaman SOP (pencarian, filter chip, modal detail).
|
||||
*/
|
||||
export function initSopInteraction() {
|
||||
// Search input
|
||||
const searchInput = document.getElementById('sop-search-input');
|
||||
const searchClear = document.getElementById('sop-search-clear');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', () => {
|
||||
if (searchClear) {
|
||||
searchClear.style.display = searchInput.value ? 'flex' : 'none';
|
||||
}
|
||||
applySopFilter();
|
||||
});
|
||||
}
|
||||
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', () => {
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
searchClear.style.display = 'none';
|
||||
applySopFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// Filter chips
|
||||
document.querySelectorAll('.sop-chip').forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sop-chip').forEach(activeChip => {
|
||||
activeChip.classList.remove('active');
|
||||
});
|
||||
chip.classList.add('active');
|
||||
applySopFilter();
|
||||
});
|
||||
});
|
||||
|
||||
// Empty state reset button
|
||||
const emptyReset = document.getElementById('sop-empty-reset');
|
||||
if (emptyReset) {
|
||||
emptyReset.addEventListener('click', () => {
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
if (searchClear) {
|
||||
searchClear.style.display = 'none';
|
||||
}
|
||||
document.querySelectorAll('.sop-chip').forEach(activeChip => {
|
||||
activeChip.classList.remove('active');
|
||||
});
|
||||
const allChip = document.querySelector('[data-filter="semua"]');
|
||||
if (allChip) {
|
||||
allChip.classList.add('active');
|
||||
}
|
||||
applySopFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// Card detail button (event delegation)
|
||||
const container = document.getElementById('sop-cards-container');
|
||||
if (container) {
|
||||
container.addEventListener('click', (event) => {
|
||||
const btn = event.target.closest('.sgc-detail-btn');
|
||||
if (btn) {
|
||||
openSopModal(btn.dataset.tabId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Modal close button
|
||||
const closeBtn = document.getElementById('sop-modal-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', closeSopModal);
|
||||
}
|
||||
|
||||
// Close on overlay click
|
||||
const overlay = document.getElementById('sop-modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) {
|
||||
closeSopModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close on Escape key
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeSopModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Kompatibilitas Mundur ---
|
||||
|
||||
/**
|
||||
* Stub kompatibilitas mundur untuk renderSopTabs.
|
||||
* @param {Array<Object>} sops - Array objek SOP
|
||||
*/
|
||||
export function renderSopTabs(sops) {
|
||||
renderSopCards(sops);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub kompatibilitas mundur untuk initSopTabsInteraction.
|
||||
*/
|
||||
export function initSopTabsInteraction() {
|
||||
initSopInteraction();
|
||||
}
|
||||
|
||||
// --- Interaksi & Animasi ---
|
||||
|
||||
/**
|
||||
* Menginisialisasi animasi scroll reveal dan counter statistik pada hero section.
|
||||
*/
|
||||
export function initAnimations() {
|
||||
// Scroll Reveal
|
||||
const revealObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
entry.target.classList.add('visible');
|
||||
revealObserver.unobserve(entry.target);
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
document.querySelectorAll('.reveal').forEach((el) => revealObserver.observe(el));
|
||||
|
||||
// Hero Counter
|
||||
const COUNT_ANIMATION_DURATION_MS = 800;
|
||||
|
||||
/**
|
||||
* Menjalankan animasi hitung angka (counter)
|
||||
* @param {Element} targetElement - Elemen target angka
|
||||
* @param {number} targetValue - Nilai akhir counter
|
||||
*/
|
||||
function animateCount(targetElement, targetValue) {
|
||||
if (!targetElement) return;
|
||||
const stepValue = targetValue / (COUNT_ANIMATION_DURATION_MS / 16);
|
||||
let currentValue = 0;
|
||||
const timerId = setInterval(() => {
|
||||
currentValue += stepValue;
|
||||
if (currentValue >= targetValue) {
|
||||
currentValue = targetValue;
|
||||
clearInterval(timerId);
|
||||
}
|
||||
targetElement.textContent = Math.floor(currentValue).toString();
|
||||
}, 16);
|
||||
}
|
||||
|
||||
const staffCounter = document.getElementById('stat-staf');
|
||||
const sopCounter = document.getElementById('stat-sop');
|
||||
|
||||
// Hitung jumlah data yang berhasil di-render (tidak termasuk template)
|
||||
const totalStaffCards = document.querySelectorAll('.staff-card').length;
|
||||
const totalSopCards = document.querySelectorAll('.sop-grid-card').length;
|
||||
|
||||
animateCount(staffCounter, totalStaffCards);
|
||||
animateCount(sopCounter, totalSopCards);
|
||||
}
|
||||
|
||||
// --- FAQ Accordion ---
|
||||
|
||||
/**
|
||||
* Membuka item FAQ yang diklik; menutup item lain yang sebelumnya
|
||||
* terbuka (perilaku accordion satu-terbuka-sekaligus).
|
||||
* @param {Element} questionEl - Elemen .faq-question yang diklik
|
||||
*/
|
||||
function toggleFaq(questionEl) {
|
||||
const item = questionEl.closest('.faq-item');
|
||||
if (!item) return;
|
||||
const isOpen = item.classList.contains('open');
|
||||
|
||||
// Tutup semua item terlebih dahulu
|
||||
document.querySelectorAll('.faq-item').forEach(faqItem => {
|
||||
faqItem.classList.remove('open');
|
||||
});
|
||||
|
||||
// Buka item yang diklik hanya jika sebelumnya tertutup
|
||||
if (!isOpen) {
|
||||
item.classList.add('open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menginisialisasi event listener klik untuk setiap accordion FAQ.
|
||||
*/
|
||||
export function initFaq() {
|
||||
document.querySelectorAll('.faq-question').forEach(questionEl => {
|
||||
questionEl.addEventListener('click', () => toggleFaq(questionEl));
|
||||
});
|
||||
}
|
||||
|
||||
// --- Render FAQ ---
|
||||
|
||||
/**
|
||||
* Merender section FAQ secara dinamis dari data-faq.json.
|
||||
* Diinjeksikan ke dalam #faq-container di index.html.
|
||||
* @param {Array<Object>} faqs - Array objek FAQ dari data-faq.json
|
||||
*/
|
||||
export function renderFaqSection(faqs) {
|
||||
const container = document.getElementById('faq-container');
|
||||
if (!container) return;
|
||||
|
||||
if (!faqs || faqs.length === 0) {
|
||||
container.innerHTML = `<p class="empty-state-msg">${t('grp_c4a_empty_faq', 'Data FAQ tidak tersedia saat ini.')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const delayClasses = ['reveal-delay-1', 'reveal-delay-2', 'reveal-delay-3'];
|
||||
|
||||
const itemsHTML = faqs.map((faq, index) => {
|
||||
const delayClass = delayClasses[index] || '';
|
||||
return `
|
||||
<div class="faq-item reveal ${delayClass}">
|
||||
<div class="faq-question">
|
||||
<span class="faq-question-text">${loc(faq.question)}</span>
|
||||
<div class="faq-icon">+</div>
|
||||
</div>
|
||||
<div class="faq-answer">
|
||||
<div class="faq-answer-inner">${loc(faq.answer)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = itemsHTML;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
# 🎓 Portal Informasi Penerimaan Mahasiswa Baru (PMB)
|
||||
**Program Studi Informatika - Universitas Tanjungpura (UNTAN)**
|
||||
|
||||

|
||||
*Pratinjau Antarmuka Halaman PMB Informatika UNTAN oleh Kelompok 02*
|
||||
|
||||
---
|
||||
|
||||
## 💡 Pengantar (Project Overview)
|
||||
|
||||
### 📌 Gambaran Umum
|
||||
Proyek ini merupakan halaman web statis yang dikembangkan untuk menyediakan informasi penerimaan mahasiswa baru secara terpusat, terstruktur, dan mudah diakses. Halaman ini bertujuan membantu calon mahasiswa dalam memahami berbagai jalur seleksi serta informasi terkait secara menyeluruh.
|
||||
|
||||
### 🎯 Fitur Utama
|
||||
|
||||
Penyajian informasi terpusat untuk seluruh jalur penerimaan:
|
||||
1. SNBP
|
||||
2. SNBT
|
||||
3. Mandiri-UNTAN
|
||||
4. SMMPTN-Barat
|
||||
|
||||
Informasi detail setiap jalur:
|
||||
1. Persyaratan
|
||||
2. Mekanisme seleksi
|
||||
3. Jadwal
|
||||
4. Biaya pendidikan
|
||||
5. Informasi beasiswa
|
||||
6. FAQ (Frequently Asked Questions)
|
||||
7. Tautan ke situs resmi dan sumber informasi terkait
|
||||
8. Tampilan responsif untuk desktop dan perangkat mobile
|
||||
|
||||
---
|
||||
|
||||
## ❓ Tanya Jawab Seputar Proyek (Project Q&A)
|
||||
|
||||
**Q: Apa fokus utama dari rancangan website ini?**
|
||||
A: Website ini dirancang sebagai pusat informasi terpadu yang merangkum empat jalur seleksi masuk UNTAN (SNBP, SNBT, SMMPTN-Barat, dan Mandiri). Pengunjung dapat melihat persyaratan, mekanisme, jadwal, rincian biaya, hingga peluang beasiswa dalam satu tampilan interaktif tanpa harus berpindah halaman.
|
||||
|
||||
**Q: Mengapa dibangun sepenuhnya sebagai halaman statis?**
|
||||
A: Pendekatan statis (HTML, CSS, Vanilla JS) menawarkan tiga keuntungan utama: **Kecepatan** muat yang luar biasa, **Keamanan** tingkat tinggi (karena tidak ada akses *database* langsung dari luar), dan kemudahan **Deployment** di hampir semua layanan *hosting*.
|
||||
|
||||
**Q: Bagaimana sistem dua bahasa (Bilingual) bekerja di website ini?**
|
||||
A: Kami menggunakan *script* kustom (`localization.js`) yang mendeteksi bahasa bawaan dari peramban pengguna (`navigator.language`). Pengguna juga dapat mengubah bahasa secara manual (ID/EN), dan pilihan tersebut akan disimpan di dalam `localStorage` sehingga preferensi tidak hilang saat halaman di-muat ulang.
|
||||
|
||||
**Q: Apakah visualisasi data di website ini bersifat statis (gambar)?**
|
||||
A: Tidak. Kami mengimplementasikan **Chart.js** untuk merender grafik perbandingan antara "Jumlah Peminat" dan "Daya Tampung" secara dinamis dan interaktif.
|
||||
|
||||
**Q: Bagaimana tingkat responsivitas website ini di perangkat seluler?**
|
||||
A: Sangat optimal. Kami menggunakan pendekatan tata letak *Grid* dan *Flexbox* mutakhir. Untuk navigasi kartu jalur masuk, kami bahkan mengintegrasikan *Horizontal Drag Slider* yang ramah sentuhan (touch-friendly) khusus untuk perangkat *mobile* dan tablet.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Navigasi & Antarmuka
|
||||
|
||||
1. Menggunakan pendekatan single-page dengan smooth scrolling
|
||||
2. Navigasi berbasis tab untuk pemilihan jalur seleksi
|
||||
3. Komponen accordion untuk FAQ dan persyaratan
|
||||
4. Layout berbasis card untuk penyajian informasi ringkas
|
||||
|
||||
---
|
||||
|
||||
## 📎 Sumber Eksternal
|
||||
Sistem menyediakan tautan menuju berbagai sumber resmi, seperti:
|
||||
|
||||
1. Situs resmi SNPMB
|
||||
2. Situs resmi Mandiri-UNTAN
|
||||
3. Situs resmi SMMPTN-Barat
|
||||
4. Portal informasi beasiswa
|
||||
5. Media sosial resmi terkait
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tumpukan Teknologi (Tech Stack)
|
||||
|
||||
| Kategori | Teknologi | Kegunaan dalam Proyek |
|
||||
| :--- | :--- | :--- |
|
||||
| **Inti (Core)** | HTML5, CSS3, Vanilla JS | Fundamental pembentuk struktur, gaya, dan logika halaman. |
|
||||
| **Visualisasi** | Chart.js 4.4 | Merender grafik batang peminat & daya tampung. |
|
||||
| **Animasi** | AOS (Animate On Scroll) | Memberikan efek transisi halus saat elemen masuk ke *viewport*. |
|
||||
| **Ikonografi** | FontAwesome 6.5 | Menyediakan aset ikon modern dan ringan berskala vektor. |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Panduan Deployment (Hosting Guide)
|
||||
Sifat proyek yang 100% statis membuatnya sangat mudah untuk di-deploy (di-online-kan). Pilih salah satu platform favorit Anda di bawah ini:
|
||||
|
||||
### Opsi 1: Vercel (Paling Direkomendasikan & Cepat)
|
||||
|
||||
1. Push proyek ke repositori GitHub, GitLab, atau Bitbucket.
|
||||
|
||||
2. Buat akun dan masuk ke Vercel.
|
||||
|
||||
3. Klik tombol Add New Project dan import repositori Anda.
|
||||
|
||||
4. Pada bagian Framework Preset, pilih Other.
|
||||
|
||||
5. Kosongkan bagian Build Command dan Output Directory.
|
||||
|
||||
6. Klik Deploy. Website Anda akan aktif dalam kurang dari 30 detik.
|
||||
|
||||
### Opsi 2: Coolify (Self-Hosted)
|
||||
|
||||
1. Masuk ke Dashboard Coolify server Anda.
|
||||
|
||||
2. Buka menu proyek lalu tambahkan resource baru (+ Add).
|
||||
|
||||
3. Pilih opsi Application dan hubungkan repositori Git Anda.
|
||||
|
||||
4. Coolify secara otomatis akan menggunakan Nixpacks untuk mendeteksi Static/HTML Site.
|
||||
|
||||
5. Kosongkan Build Command dan Install Command.
|
||||
|
||||
6. Klik Deploy.
|
||||
|
||||
### Opsi 3: GitHub Pages (Paling Praktis)
|
||||
|
||||
1. Pastikan kode sudah di-push ke repositori GitHub.
|
||||
|
||||
2. Masuk ke halaman repositori Anda, klik tab Settings.
|
||||
|
||||
3. Di panel sebelah kiri, pilih menu Pages.
|
||||
|
||||
4. Pada bagian Source, pilih Deploy from a branch.
|
||||
|
||||
5. Pilih branch utama dan pastikan foldernya diatur ke / (root).
|
||||
|
||||
6. Klik Save.
|
||||
|
||||
### Opsi 4: Netlify (Sistem Drag & Drop)
|
||||
|
||||
1. Masuk ke Netlify.
|
||||
|
||||
2. Masuk ke menu Sites -> Klik Add new site.
|
||||
|
||||
3. Kita bisa memilih Import an existing project (lewat Git) atau Deploy manually (cukup seret dan lepas folder proyek ke browser).
|
||||
|
||||
4. Jika lewat Git, biarkan Build command dan Publish directory kosong.
|
||||
|
||||
5. Klik Deploy site.
|
||||
|
||||
---
|
||||
|
||||
## 👥 Tim Pengembang (Credits)
|
||||
Disusun dan dikembangkan oleh Kelompok 02 untuk memenuhi standar tugas perancangan antarmuka Halaman Statis.
|
||||
|
||||
Nama Kelompok : Habis Ini Seminar
|
||||
|
||||
---
|
||||
|
||||
## 📂 Struktur Direktori (Folder Structure)
|
||||
|
||||
Pastikan struktur direktori Anda sesuai sebelum menjalankan atau men-deploy proyek ini:
|
||||
|
||||
```text
|
||||
📦 project-root/
|
||||
┣ 📜 localization.js # Script logika terjemahan & State Management
|
||||
┃ 🖼️ logo.png # Logo Universitas/Prodi
|
||||
┃ 🖼️ footer.png # Aset ornamen footer
|
||||
┣ 📜 index.html # Halaman utama aplikasi (Entry point)
|
||||
┣ 📜 style.css # Berkas tata letak dan desain (Styling)
|
||||
┣ 🖼️ Screenshot.png # Gambar pratinjau halaman
|
||||
┗ 📜 README.md # Dokumentasi proyek
|
||||
|
Before Width: | Height: | Size: 118 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.375 1.25C3.71196 1.25 3.07607 1.51339 2.60723 1.98223C2.13839 2.45107 1.875 3.08696 1.875 3.75V11.25C1.875 11.913 2.13839 12.5489 2.60723 13.0178C3.07607 13.4866 3.71196 13.75 4.375 13.75H13.125V1.25H4.375ZM6.875 3.125H11.25V4.375H6.875V3.125ZM3.125 11.25C3.125 10.9185 3.2567 10.6005 3.49112 10.3661C3.72554 10.1317 4.04348 10 4.375 10H11.875V12.5H4.375C4.04348 12.5 3.72554 12.3683 3.49112 12.1339C3.2567 11.8995 3.125 11.5815 3.125 11.25Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 573 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.448 0.105966C15.3089 0.0362835 15.1555 0 15 0C14.8445 0 14.6911 0.0362835 14.552 0.105966L2.552 6.10597L3.448 7.89397L15 2.11997L26.552 7.89597L27.448 6.10797L15.448 0.105966ZM28 12H30V9.99997H0V12H2V28H0V30H8V16H18V30H30V28H28V12Z" fill="black"/>
|
||||
<path d="M16 30V18H10V30H16Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 409 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.7498 10H11.3332V11.3333H12.7498M12.7498 7.33333H11.3332V8.66667H12.7498M14.1665 12.6667H8.49984V11.3333H9.9165V10H8.49984V8.66667H9.9165V7.33333H8.49984V6H14.1665M7.08317 4.66667H5.6665V3.33333H7.08317M7.08317 7.33333H5.6665V6H7.08317M7.08317 10H5.6665V8.66667H7.08317M7.08317 12.6667H5.6665V11.3333H7.08317M4.24984 4.66667H2.83317V3.33333H4.24984M4.24984 7.33333H2.83317V6H4.24984M4.24984 10H2.83317V8.66667H4.24984M4.24984 12.6667H2.83317V11.3333H4.24984M8.49984 4.66667V2H1.4165V14H15.5832V4.66667H8.49984Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 642 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0.53125C3.858 0.53125 0.5 4.09912 0.5 8.5C0.5 12.9009 3.858 16.4688 8 16.4688C12.142 16.4688 15.5 12.9009 15.5 8.5C15.5 4.09912 12.142 0.53125 8 0.53125ZM8 15.9375C4.14025 15.9375 1 12.601 1 8.5H15C15 12.601 11.8597 15.9375 8 15.9375Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 366 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 1.33337C10.3536 1.33337 10.6928 1.47385 10.9428 1.7239C11.1929 1.97395 11.3333 2.31309 11.3333 2.66671V6.00004H12.6667C13.0203 6.00004 13.3594 6.14052 13.6095 6.39056C13.8595 6.64061 14 6.97975 14 7.33337V13.9334C14 14.0297 13.981 14.125 13.9442 14.214C13.9073 14.303 13.8533 14.3838 13.7852 14.4519C13.7171 14.52 13.6363 14.574 13.5473 14.6109C13.4583 14.6477 13.363 14.6667 13.2667 14.6667H2.73333C2.63703 14.6667 2.54167 14.6477 2.4527 14.6109C2.36373 14.574 2.28288 14.52 2.21479 14.4519C2.14669 14.3838 2.09268 14.303 2.05582 14.214C2.01897 14.125 2 14.0297 2 13.9334V2.66671C2 2.31309 2.14048 1.97395 2.39052 1.7239C2.64057 1.47385 2.97971 1.33337 3.33333 1.33337H10ZM10 2.66671H3.33333V13.3334H4.66667V11.3334C4.66667 10.8029 4.87738 10.2942 5.25245 9.91916C5.62753 9.54409 6.13623 9.33337 6.66667 9.33337C7.1971 9.33337 7.70581 9.54409 8.08088 9.91916C8.45595 10.2942 8.66667 10.8029 8.66667 11.3334V13.3334H10V2.66671ZM12.6667 7.33337H11.3333V13.3334H12.6667V7.33337ZM6.66667 10.6667C6.50338 10.6667 6.34578 10.7267 6.22375 10.8352C6.10173 10.9437 6.02377 11.0932 6.00467 11.2554L6 11.3334V13.3334H7.33333V11.3334C7.33333 11.1566 7.2631 10.987 7.13807 10.862C7.01305 10.7369 6.84348 10.6667 6.66667 10.6667ZM6.66667 4.00004C6.84348 4.00004 7.01305 4.07028 7.13807 4.1953C7.2631 4.32033 7.33333 4.4899 7.33333 4.66671V5.33337H8C8.17681 5.33337 8.34638 5.40361 8.4714 5.52864C8.59643 5.65366 8.66667 5.82323 8.66667 6.00004C8.66667 6.17685 8.59643 6.34642 8.4714 6.47144C8.34638 6.59647 8.17681 6.66671 8 6.66671H7.33333V7.33337C7.33333 7.51018 7.2631 7.67975 7.13807 7.80478C7.01305 7.9298 6.84348 8.00004 6.66667 8.00004C6.48986 8.00004 6.32029 7.9298 6.19526 7.80478C6.07024 7.67975 6 7.51018 6 7.33337V6.66671H5.33333C5.15652 6.66671 4.98695 6.59647 4.86193 6.47144C4.7369 6.34642 4.66667 6.17685 4.66667 6.00004C4.66667 5.82323 4.7369 5.65366 4.86193 5.52864C4.98695 5.40361 5.15652 5.33337 5.33333 5.33337H6V4.66671C6 4.4899 6.07024 4.32033 6.19526 4.1953C6.32029 4.07028 6.48986 4.00004 6.66667 4.00004Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.75 5.625L7.5 0.625L1.25 5.625V6.875H2.5V13.125H5.625V10.625C5.625 10.1277 5.82254 9.65081 6.17417 9.29917C6.52581 8.94754 7.00272 8.75 7.5 8.75C7.99728 8.75 8.47419 8.94754 8.82583 9.29917C9.17746 9.65081 9.375 10.1277 9.375 10.625V13.125H12.5V6.875H13.75V5.625Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 395 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2352 0.5V13.4231L27.735 23.2661C28.149 23.7428 28.407 24.316 28.4792 24.9199C28.5514 25.5238 28.435 26.134 28.1433 26.6805C27.8516 27.227 27.3964 27.6875 26.83 28.0094C26.2636 28.3312 25.609 28.5013 24.9412 28.5H4.05878C3.39099 28.5013 2.73635 28.3312 2.16998 28.0094C1.6036 27.6875 1.14845 27.227 0.856745 26.6805C0.565044 26.134 0.448617 25.5238 0.520825 24.9199C0.593034 24.316 0.850952 23.7428 1.26498 23.2661L9.76475 13.4231V0.5M6.21332 0.5H22.7867" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 635 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.55342 5.86L5.33342 6.304V8.66667H4.00009V5.36667H4.01009L7.52209 4.088C7.68475 4.026 7.86209 3.99467 8.04342 4.00067C8.40565 4.01009 8.75599 4.13194 9.04589 4.34934C9.33579 4.56673 9.5509 4.86891 9.66142 5.214C9.78542 5.60289 9.89875 5.86533 10.0014 6.00133C10.3119 6.41489 10.7144 6.75054 11.177 6.98172C11.6396 7.2129 12.1496 7.33328 12.6668 7.33333V8.66667C11.9783 8.66723 11.2983 8.51525 10.6756 8.22165C10.0529 7.92806 9.50301 7.50014 9.06542 6.96867L8.67809 9.16667L10.0001 10.4467V15.3333H8.66675V11.3427L7.30009 10.018L6.66875 12.8833L2.07275 12.0733L2.30475 10.76L5.58742 11.3387L6.55342 5.86ZM9.00009 3.66667C8.64647 3.66667 8.30733 3.52619 8.05728 3.27614C7.80723 3.02609 7.66675 2.68696 7.66675 2.33333C7.66675 1.97971 7.80723 1.64057 8.05728 1.39052C8.30733 1.14048 8.64647 1 9.00009 1C9.35371 1 9.69285 1.14048 9.9429 1.39052C10.1929 1.64057 10.3334 1.97971 10.3334 2.33333C10.3334 2.68696 10.1929 3.02609 9.9429 3.27614C9.69285 3.52619 9.35371 3.66667 9.00009 3.66667Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.6877 8.00625C19.4492 8.24564 19.166 8.43572 18.8541 8.56564C18.5422 8.69557 18.2078 8.76279 17.8699 8.76346C17.1875 8.76483 16.5326 8.49508 16.0491 8.01354C15.5657 7.53201 15.2933 6.87814 15.2919 6.19578C15.2905 5.51342 15.5603 4.85847 16.0418 4.375M17.5002 8.75V13.125M17.5002 13.125C19.4678 15.3764 21.8178 17.2624 24.4418 18.6958C27.8252 20.5333 29.1668 22.3563 29.1668 25.2729C29.0721 27.1412 28.5746 28.9669 27.7085 30.625H7.29183C6.4257 28.9669 5.92823 27.1412 5.8335 25.2729C5.8335 22.4146 7.17516 20.5333 10.5585 18.6958C13.1825 17.2624 15.5326 15.3764 17.5002 13.125Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 775 B |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 749 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 500 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 548 KiB |
|
Before Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 127 KiB |