Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion static/script.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,120 @@
// script.js — DevPath client-side logic
//
// Responsibilities:
// - Theme initialisation (dark / light) with localStorage persistence
// - Mobile navigation toggle
// - Skill chip manager (add/remove skills)
// - Form validation with per-field error messages
// - Recommendation API call and loading states
// - Result card rendering
// - Code viewer panel (detail page)


// ============================================================
// THEME ENGINE
// ============================================================
// The theme system works in three parts:
//
// Part A — Anti-FOUC inline script (in <head> of each template):
// Sets html[data-theme] synchronously before the stylesheet is
// evaluated, so the browser paints the correct colours on frame 1.
//
// Part B — initTheme() (runs immediately below):
// Syncs the toggle button aria-pressed + aria-label with the
// already-applied theme. Adds the "theme-ready" class on the
// next animation frame so CSS transitions become active only
// AFTER the initial paint (preventing a colour transition flash
// when the page first loads).
//
// Part C — applyTheme(theme) (called on button click):
// The single source of truth for all theme changes. Updates
// data-theme, localStorage, aria-pressed, aria-label, and an
// aria-live region so screen readers announce the change.
// ============================================================

(function () {

// ---- Part B: sync button state once DOM is ready ----------
function initTheme() {
var html = document.documentElement;
var theme = html.dataset.theme || "light";

// Sync every toggle button on the page (desktop + mobile versions)
document.querySelectorAll(".theme-toggle").forEach(function (btn) {
var isDark = theme === "dark";
// aria-pressed = true when dark mode is ON
btn.setAttribute("aria-pressed", isDark ? "true" : "false");
// aria-label describes what clicking WILL do (not what IS active),
// which is the recommended accessible pattern for toggle buttons.
btn.setAttribute("aria-label",
isDark ? "Switch to light mode" : "Switch to dark mode"
);
});

// Add .theme-ready on the NEXT frame so CSS transitions are
// suppressed during the initial render (avoids colour flash).
requestAnimationFrame(function () {
html.classList.add("theme-ready");
});
Comment thread
DelacroixAD marked this conversation as resolved.
}

// ---- Part C: apply a theme change -------------------------
function applyTheme(theme) {
var html = document.documentElement;
var isDark = theme === "dark";

// 1. Apply via data attribute — CSS [data-theme="dark"] picks this up
html.dataset.theme = theme;

// 2. Persist the user's choice across sessions
try { localStorage.setItem("devpath-theme", theme); } catch (e) { /* private browsing may block */ }

// 3. Update every toggle button's accessible state
document.querySelectorAll(".theme-toggle").forEach(function (btn) {
btn.setAttribute("aria-pressed", isDark ? "true" : "false");
btn.setAttribute("aria-label",
isDark ? "Switch to light mode" : "Switch to dark mode"
);
});

// 4. Announce the change to screen readers via a visually-hidden
// aria-live="polite" region injected once into the DOM.
var liveRegion = document.getElementById("theme-announce");
if (!liveRegion) {
liveRegion = document.createElement("span");
liveRegion.id = "theme-announce";
// Visually hidden but readable by screen readers
liveRegion.setAttribute("role", "status");
liveRegion.setAttribute("aria-live", "polite");
liveRegion.style.cssText =
"position:absolute;width:1px;height:1px;padding:0;overflow:hidden;" +
"clip:rect(0,0,0,0);white-space:nowrap;border:0;";
document.body.appendChild(liveRegion);
}
liveRegion.textContent = isDark ? "Dark mode enabled." : "Light mode enabled.";
}
Comment thread
DelacroixAD marked this conversation as resolved.


document.addEventListener("click", function (evt) {
var btn = evt.target.closest(".theme-toggle");
if (!btn) return;
var current = document.documentElement.dataset.theme || "light";
applyTheme(current === "dark" ? "light" : "dark");
});

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initTheme);
} else {
initTheme();
}

}());


// ============================================================
// Detect which page we are on
// ============================================================
var isIndexPage = !!document.getElementById("recommend-form");
// !! trick turns the DOM result into a simple true/false
var isIndexPage = !!document.getElementById("recommend-form");
// PROJECT_ID is set by the server only on detail pages, so if it's missing we're elsewhere
Expand All @@ -28,7 +132,7 @@ var errorMsg = document.getElementById('github-modal-error');
// ============================================================
(function initMobileNav() {
var toggle = document.getElementById("nav-mobile-toggle"); //hamburger button
var menu = document.getElementById("nav-mobile-menu"); //dropdown menu
var menu = document.getElementById("nav-mobile-menu"); //dropdown menu

// Nothing to do if the nav isn't on this page, just bail out
if (!toggle || !menu) return;
Expand All @@ -37,6 +141,8 @@ var errorMsg = document.getElementById('github-modal-error');
// classList.toggle returns true if class was added, false if removed
var isOpen = menu.classList.toggle("open");
toggle.classList.toggle("open", isOpen);
// aria-expanded reflects whether the controlled menu is expanded
toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
// Keep aria-expanded in sync so screen readers know if menu is open or closed
toggle.setAttribute("aria-expanded", isOpen);
});
Expand All @@ -46,6 +152,8 @@ var errorMsg = document.getElementById('github-modal-error');
link.addEventListener("click", function () {
menu.classList.remove("open");
toggle.classList.remove("open");
// FIX: reset aria-expanded when menu closes via link click
toggle.setAttribute("aria-expanded", "false");
});
});
})();
Expand Down Expand Up @@ -930,3 +1038,4 @@ if (scrollTopBtn) {
window.addEventListener('scroll', handleScroll);
scrollTopBtn.addEventListener('click', scrollToTop);
}
}
Loading
Loading