// ObsWiki Frontend JavaScript class ObsWiki { constructor() { this.searchInput = document.getElementById("search"); this.currentUser = null; this.searchTimeout = null; this.init(); } init() { this.setupSearch(); this.setupAuth(); this.setupWikiLinks(); this.setupTags(); } setupSearch() { if (this.searchInput) { this.searchInput.addEventListener("input", (e) => { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.performSearch(e.target.value); }, 300); }); this.searchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.performSearch(e.target.value); } }); } } async performSearch(query) { if (query.length < 2) { this.hideSearchResults(); return; } try { const response = await fetch( `/api/search?q=${encodeURIComponent(query)}&limit=5`, ); if (response.ok) { const results = await response.json(); this.showSearchResults(results); } } catch (error) { console.error("Search error:", error); } } showSearchResults(results) { let resultsContainer = document.getElementById("search-results"); if (!resultsContainer) { resultsContainer = document.createElement("div"); resultsContainer.id = "search-results"; this.searchInput.parentNode.appendChild(resultsContainer); } if (results.length === 0) { resultsContainer.innerHTML = '
No results found
'; } else { resultsContainer.innerHTML = results .map( (page) => `
${this.escapeHtml(page.title)}
${this.escapeHtml(page.path)}
`, ) .join(""); } resultsContainer.style.display = "block"; } hideSearchResults() { const resultsContainer = document.getElementById("search-results"); if (resultsContainer) { resultsContainer.style.display = "none"; } } setupAuth() { const token = localStorage.getItem("obswiki_token"); if (token) { this.verifyToken(token); } // Handle login form const loginForm = document.getElementById("login-form"); if (loginForm) { loginForm.addEventListener("submit", (e) => { e.preventDefault(); this.handleLogin(new FormData(loginForm)); }); } // Handle register form const registerForm = document.getElementById("register-form"); if (registerForm) { registerForm.addEventListener("submit", (e) => { e.preventDefault(); this.handleRegister(new FormData(registerForm)); }); } // Handle logout const logoutBtn = document.getElementById("logout-btn"); if (logoutBtn) { logoutBtn.addEventListener("click", (e) => { e.preventDefault(); this.logout(); }); } } async handleLogin(formData) { try { const response = await fetch("/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: formData.get("username"), password: formData.get("password"), }), }); if (response.ok) { const data = await response.json(); this.setAuth(data.token, data.user); window.location.href = "/"; } else { this.showError("Invalid username or password"); } } catch (error) { console.error("Login error:", error); this.showError("Login failed"); } } async handleRegister(formData) { try { const response = await fetch("/auth/register", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: formData.get("username"), email: formData.get("email"), password: formData.get("password"), }), }); if (response.ok) { const data = await response.json(); this.setAuth(data.token, data.user); window.location.href = "/"; } else { this.showError("Registration failed"); } } catch (error) { console.error("Register error:", error); this.showError("Registration failed"); } } async verifyToken(token) { try { const response = await fetch("/api/me", { headers: { Authorization: `Bearer ${token}`, }, }); if (response.ok) { const user = await response.json(); this.setAuth(token, user, false); } else { localStorage.removeItem("obswiki_token"); } } catch (error) { console.error("Token verification error:", error); localStorage.removeItem("obswiki_token"); } } setAuth(token, user, store = true) { if (store) { localStorage.setItem("obswiki_token", token); } this.currentUser = user; this.updateAuthUI(); } logout() { localStorage.removeItem("obswiki_token"); this.currentUser = null; this.updateAuthUI(); window.location.href = "/"; } updateAuthUI() { const authContainer = document.querySelector(".auth"); if (!authContainer) return; if (this.currentUser) { authContainer.innerHTML = ` Welcome, ${this.escapeHtml(this.currentUser.username)} Logout `; const logoutBtn = document.getElementById("logout-btn"); if (logoutBtn) { logoutBtn.addEventListener("click", (e) => { e.preventDefault(); this.logout(); }); } } else { authContainer.innerHTML = ` Login Register `; } } setupWikiLinks() { // Handle wiki link clicks for better navigation document.addEventListener("click", (e) => { if (e.target.classList.contains("wiki-link")) { // Add loading state or other UX improvements here } }); } setupTags() { // Handle tag clicks document.addEventListener("click", (e) => { if (e.target.classList.contains("tag")) { const tag = e.target.getAttribute("data-tag"); if (tag) { this.searchByTag(tag); } } }); } searchByTag(tag) { if (this.searchInput) { this.searchInput.value = `#${tag}`; this.performSearch(`#${tag}`); } } showError(message) { // Create or update error message let errorDiv = document.getElementById("error-message"); if (!errorDiv) { errorDiv = document.createElement("div"); errorDiv.id = "error-message"; errorDiv.id = "error-message"; document.body.insertBefore(errorDiv, document.body.firstChild); } errorDiv.textContent = message; errorDiv.style.display = "block"; // Auto-hide after 5 seconds setTimeout(() => { errorDiv.style.display = "none"; }, 5000); } escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } encodePath(path) { // Split path by '/' and encode each component separately, then rejoin with '/' return path .split("/") .map((component) => encodeURIComponent(component)) .join("/"); } // Utility method for making authenticated requests async authenticatedFetch(url, options = {}) { const token = localStorage.getItem("obswiki_token"); if (token) { options.headers = { ...options.headers, Authorization: `Bearer ${token}`, }; } return fetch(url, options); } } // Initialize when DOM is loaded document.addEventListener("DOMContentLoaded", () => { window.obswiki = new ObsWiki(); });