2 Commits

14 changed files with 2090 additions and 2662 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

File diff suppressed because it is too large Load Diff
+99
View File
@@ -1,3 +1,29 @@
/**
* localization.js — HarusSelesaiKP
*
* ANTI-FLICKER STRATEGY:
* 1. IIFE `initLanguage()` runs SYNCHRONOUSLY (no defer/async) to set
* `window.__currentLang` before the browser paints any content.
* 2. HTML body carries `lang-pending` class; body is hidden via CSS until
* `applyTranslations()` fires on DOMContentLoaded and removes the class.
* 3. `languageChanged` CustomEvent signals dynamic sections (Facilities,
* Campus, Culinary, LivingCost) to re-render in the new language.
*/
// ─── 1. Determine language SYNCHRONOUSLY (before first paint) ────────────────
(function initLanguage() {
try {
const saved = localStorage.getItem("lang");
const browserLang = navigator.language?.startsWith("id") ? "id" : "en";
window.__currentLang = saved || browserLang;
} catch (e) {
window.__currentLang = "id";
}
// Stamp <html lang> immediately so screen readers get the right language
document.documentElement.lang = window.__currentLang;
})();
// ─── 2. Translation data ─────────────────────────────────────────────────────
const translations = {
id: {
// Kelompok HarusSelesaiKP (skp)
@@ -330,3 +356,76 @@ const translations = {
skp_cost_type_transport: "Transport",
},
};
// ─── 3. Public translation helper ────────────────────────────────────────────
/**
* t(key) — returns translated string for the active language.
* Falls back to Indonesian if key is missing in the active language.
*/
function t(key) {
const lang = window.__currentLang || "id";
return translations[lang]?.[key] ?? translations["id"]?.[key] ?? key;
}
// ─── 4. Apply translations to all [data-i18n] elements ───────────────────────
function applyTranslations(lang) {
window.__currentLang = lang;
document.documentElement.lang = lang;
try {
localStorage.setItem("lang", lang);
} catch (e) {
// localStorage may be blocked (private mode, etc.) — fail silently
}
document.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.dataset.i18n;
const text = t(key);
if (text && text !== key) {
el.textContent = text;
}
});
// Signal dynamic components to re-render
document.dispatchEvent(new CustomEvent("languageChanged"));
}
// ─── 5. Toggle language (called by UI buttons) ───────────────────────────────
function toggleLanguage() {
const next = window.__currentLang === "id" ? "en" : "id";
applyTranslations(next);
updateLanguageButtonUI(next);
}
function updateLanguageButtonUI(lang) {
document.querySelectorAll(".language-toggle-btn").forEach((btn) => {
const idSpan = btn.querySelector(".lang-id");
const enSpan = btn.querySelector(".lang-en");
if (idSpan && enSpan) {
if (lang === "id") {
idSpan.className = "lang-id font-bold text-white";
enSpan.className = "lang-en font-normal opacity-50";
} else {
idSpan.className = "lang-id font-normal opacity-50";
enSpan.className = "lang-en font-bold text-white";
}
}
});
}
// ─── 6. Boot on DOMContentLoaded ─────────────────────────────────────────────
document.addEventListener("DOMContentLoaded", () => {
// Apply translations to all static [data-i18n] nodes
applyTranslations(window.__currentLang);
// Sync language button label
updateLanguageButtonUI(window.__currentLang);
// Wire up toggle buttons
document.querySelectorAll(".language-toggle-btn").forEach((btn) => {
btn.addEventListener("click", toggleLanguage);
});
// ── Reveal body: remove anti-flicker class after translations applied ──
document.body.classList.remove("lang-pending");
});
+297
View File
@@ -1604,3 +1604,300 @@ body {
}
}
}
/* ============================================================
ANTI-FLICKER — localization.js sets window.__currentLang
synchronously; body stays hidden until DOMContentLoaded
fires applyTranslations() and removes this class.
============================================================ */
body.lang-pending {
visibility: hidden;
}
/* ============================================================
MOVED FROM index.html <style> TAG
(Originally inline; moved here so the browser can cache it)
============================================================ */
/* ===== MOBILE NAV STABILITY FIX =====
Prevents logo bar from collapsing/shifting on Android when
address bar hides/shows and triggers viewport reflow.
===================================== */
#nav-wrapper {
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 50;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
will-change: box-shadow;
}
#nav-wrapper > div:first-child {
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
@media (max-width: 1023px) {
#nav-wrapper > div:first-child {
min-height: 0 !important;
max-height: none;
}
}
#pengumuman-bar {
z-index: 40;
}
#pengumuman-bar.is-stuck {
position: fixed;
left: 0;
right: 0;
z-index: 40;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Skip Link Styling */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #003150;
color: white;
padding: 8px 16px;
z-index: 100;
transition: top 0.3s;
}
.skip-link:focus {
top: 0;
}
/* Focus visible for better keyboard navigation */
:focus-visible {
outline: 3px solid #feb401;
outline-offset: 2px;
}
/* Mega Menu Styles */
.hero-pagination .swiper-pagination-bullet {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
display: inline-block;
margin: 0 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.hero-pagination .swiper-pagination-bullet-active {
background: rgba(255, 255, 255, 1);
width: 24px;
border-radius: 4px;
}
.mega-panel {
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
position: absolute;
top: 100%;
left: 0;
right: 0;
}
.mega-panel.active {
opacity: 1;
visibility: visible;
}
/* Overlay transition */
#mega-overlay {
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
}
#mega-overlay.active {
opacity: 1;
visibility: visible;
}
.mega-link-card {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
background: white;
transition: all 0.2s ease;
}
.mega-link-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-2px);
}
.mega-link-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
}
.nav-menu-btn.active {
background: rgba(255, 255, 255, 0.9);
color: #1e40af;
}
.nav-menu-btn:hover {
background: rgba(255, 255, 255, 0.8);
}
/* Mega menu positioning - no gap */
#main-nav {
position: relative;
}
#mega-menu-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
pointer-events: none;
}
#mega-menu-container .mega-panel {
pointer-events: auto;
}
/* Keep pointer bridge under nav to prevent hover loss */
.nav-menu-item {
position: relative;
}
.nav-menu-item::after {
content: "";
position: absolute;
bottom: -12px;
left: 0;
right: 0;
height: 12px;
}
/* ===== Luco Chatbot Animations ===== */
@keyframes slideInRight {
from {
transform: translateX(100px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeInUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes lucoPulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
#luco-container {
animation: slideInRight 0.5s ease-out forwards;
}
#luco-bubble {
animation: fadeInUp 0.3s ease-out forwards;
width: min(18rem, calc(100vw - 2rem));
}
#luco-btn {
animation: lucoPulse 2s ease-in-out infinite;
}
#luco-btn:hover {
animation: none;
}
/* Typing indicator */
.typing-indicator span {
display: inline-block;
animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-6px);
}
}
/* Luco speech bubble arrow */
#luco-bubble .bubble-arrow {
position: absolute;
right: 24px;
bottom: -6px;
width: 12px;
height: 12px;
background: white;
transform: rotate(45deg);
border-right: 1px solid #e2e8f0;
border-bottom: 1px solid #e2e8f0;
}