318 lines
7.9 KiB
JavaScript
318 lines
7.9 KiB
JavaScript
// 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();
|
|
});
|
|
|