This commit is contained in:
2025-08-10 10:31:10 -04:00
commit db767dcabc
26 changed files with 8170 additions and 0 deletions

317
static/js/script.js Normal file
View File

@@ -0,0 +1,317 @@
// 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 =
'<div>No results found</div>';
} else {
resultsContainer.innerHTML = results
.map(
(page) => `
<a href="/wiki/${this.encodePath(page.path)}">
<div>${this.escapeHtml(page.title)}</div>
<div>${this.escapeHtml(page.path)}</div>
</a>
`,
)
.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 = `
<span>Welcome, ${this.escapeHtml(this.currentUser.username)}</span>
<a href="#" id="logout-btn">Logout</a>
`;
const logoutBtn = document.getElementById("logout-btn");
if (logoutBtn) {
logoutBtn.addEventListener("click", (e) => {
e.preventDefault();
this.logout();
});
}
} else {
authContainer.innerHTML = `
<a href="/auth/login">Login</a>
<a href="/auth/register">Register</a>
`;
}
}
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();
});