// Console-based authentication window.login = async (password) => { if (!password) { console.error('Usage: login("your-password")'); return; } try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); const data = await res.json(); if (res.ok) { console.log( data.firstTime ? "Password set! Reloading..." : "Logged in! Reloading...", ); setTimeout(() => location.reload(), 500); } else { console.error(data.error || "Login failed"); } } catch (err) { console.error("Login error:", err); } }; window.logout = async () => { try { await fetch("/api/auth/logout", { method: "POST" }); console.log("Logged out! Reloading..."); setTimeout(() => location.reload(), 500); } catch (err) { console.error("Logout error:", err); } }; // Modal functions function showAddModal() { document.getElementById("add-modal").classList.add("active"); } function hideAddModal() { document.getElementById("add-modal").classList.remove("active"); } function showEditModal() { document.getElementById("edit-modal").classList.add("active"); } function hideEditModal() { document.getElementById("edit-modal").classList.remove("active"); } // Tab switching document.addEventListener("DOMContentLoaded", () => { const tabs = document.querySelectorAll(".modal-tabs .tab"); tabs.forEach((tab) => { tab.addEventListener("click", () => { const tabId = tab.dataset.tab; // Update tab buttons tabs.forEach((t) => t.classList.remove("active")); tab.classList.add("active"); // Update tab content document .querySelectorAll(".tab-content") .forEach((c) => c.classList.remove("active")); document.getElementById("tab-" + tabId).classList.add("active"); }); }); // URL preview on input const urlInput = document.querySelector('#link-form input[name="url"]'); if (urlInput) { let debounceTimer; urlInput.addEventListener("input", (e) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => fetchPreview(e.target.value), 500); }); } // Gallery hover cycling const galleries = document.querySelectorAll('.grid-item-images[data-gallery="true"]'); galleries.forEach((gallery) => { const images = gallery.querySelectorAll('img'); const indicator = gallery.parentElement.querySelector('.gallery-indicator'); if (images.length < 2) return; let currentIndex = 0; let cycleInterval = null; const showImage = (index) => { images.forEach((img, i) => { img.classList.toggle('active', i === index); }); if (indicator) { indicator.textContent = `${index + 1}/${images.length}`; } }; const startCycling = () => { if (cycleInterval) return; cycleInterval = setInterval(() => { currentIndex = (currentIndex + 1) % images.length; showImage(currentIndex); }, 2000); }; const stopCycling = () => { if (cycleInterval) { clearInterval(cycleInterval); cycleInterval = null; } // Reset to first image currentIndex = 0; showImage(0); }; gallery.parentElement.addEventListener('mouseenter', startCycling); gallery.parentElement.addEventListener('mouseleave', stopCycling); }); }); // Fetch URL preview async function fetchPreview(url) { const preview = document.getElementById("link-preview"); if (!url) { preview.classList.remove("active"); preview.innerHTML = ""; return; } try { const res = await fetch("/api/preview", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), }); if (!res.ok) { const data = await res.json(); preview.innerHTML = `
${data.error || "Failed to fetch preview"}
`; preview.classList.add("active"); return; } const data = await res.json(); preview.dataset.preview = JSON.stringify(data); let html = ""; if (data.imageUrl) { html += `Preview`; } if (data.title) { html += `
${escapeHtml(data.title)}
`; } if (data.description) { html += `
${escapeHtml(data.description)}
`; } if (data.isEmbed) { html += `
${escapeHtml(data.provider.toUpperCase())} VIDEO
`; } preview.innerHTML = html || "
No preview available
"; preview.classList.add("active"); // Auto-fill title if empty const titleInput = document.querySelector('#link-form input[name="title"]'); if (titleInput && !titleInput.value && data.title) { titleInput.value = data.title; } // Auto-fill description if empty const descriptionInput = document.querySelector( '#link-form textarea[name="description"]', ); if (descriptionInput && !descriptionInput.value && data.description) { descriptionInput.value = data.description; } } catch (err) { console.error("Preview error:", err); preview.innerHTML = '
Failed to fetch preview
'; preview.classList.add("active"); } } // Submit link form async function submitLink(event) { event.preventDefault(); const form = event.target; const url = form.url.value; const tags = form.tags.value ? form.tags.value .split(",") .map((t) => t.trim()) .filter(Boolean) : []; // Get preview data const preview = document.getElementById("link-preview"); const previewData = preview.dataset.preview ? JSON.parse(preview.dataset.preview) : {}; // Use form values, falling back to preview data const title = form.title.value || previewData.title || null; const description = form.description.value || previewData.description || null; try { const body = { url, title, description, tags, imageUrl: previewData.imageUrl || null, }; if (previewData.isEmbed) { body.provider = previewData.provider; body.videoId = previewData.videoId; body.embedHtml = previewData.embedHtml; } const res = await fetch("/api/items/from-link", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to add item"); } } catch (err) { console.error("Submit error:", err); alert("Failed to add item"); } return false; } // Submit upload form async function submitUpload(event) { event.preventDefault(); const form = event.target; const formData = new FormData(form); try { const res = await fetch("/api/items/upload", { method: "POST", body: formData, }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to upload"); } } catch (err) { console.error("Upload error:", err); alert("Failed to upload"); } return false; } // Submit quote form async function submitQuote(event) { event.preventDefault(); const form = event.target; const text = form.text.value; const source = form.source.value || null; const sourceUrl = form.sourceUrl.value || null; const tags = form.tags.value ? form.tags.value .split(",") .map((t) => t.trim()) .filter(Boolean) : []; try { const res = await fetch("/api/items/quote", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, source, sourceUrl, tags }), }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to add quote"); } } catch (err) { console.error("Submit error:", err); alert("Failed to add quote"); } return false; } // Edit item function editItem(id) { showEditModal(); } // Submit edit form async function submitEdit(event) { event.preventDefault(); const form = event.target; const id = form.id.value; const title = form.title.value || null; const description = form.description.value || null; const linkUrl = form.linkUrl.value || null; const tags = form.tags.value ? form.tags.value .split(",") .map((t) => t.trim()) .filter(Boolean) : []; try { const res = await fetch(`/api/items/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, description, linkUrl, tags }), }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to update"); } } catch (err) { console.error("Update error:", err); alert("Failed to update"); } return false; } // Refresh metadata async function refreshMetadata(id) { try { // Fetch current item data const itemRes = await fetch(`/api/items/${id}`); if (!itemRes.ok) { alert("Failed to fetch item"); return; } const item = await itemRes.json(); if (!item.linkUrl) { alert("Item has no link URL"); return; } // Fetch fresh metadata const previewRes = await fetch("/api/preview", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: item.linkUrl }), }); if (!previewRes.ok) { const data = await previewRes.json(); alert(data.error || "Failed to fetch metadata"); return; } const preview = await previewRes.json(); // Check if user has made manual edits const titleChanged = item.title && preview.title && item.title !== preview.title; const descChanged = item.description && preview.description && item.description !== preview.description; const imageChanged = item.thumbnailSourceUrl && preview.imageUrl && item.thumbnailSourceUrl !== preview.imageUrl; if (titleChanged || descChanged || imageChanged) { let msg = "This will overwrite your changes:\n"; if (titleChanged) msg += `\nTitle: "${item.title}" → "${preview.title}"`; if (descChanged) msg += `\nDescription will be replaced`; if (imageChanged) msg += `\nImage will be replaced`; msg += "\n\nContinue?"; if (!confirm(msg)) return; } // Proceed with refresh const res = await fetch(`/api/items/${id}/refresh`, { method: "POST" }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to refresh"); } } catch (err) { console.error("Refresh error:", err); alert("Failed to refresh"); } } // Replace media async function submitReplaceMedia(event, id) { event.preventDefault(); const form = event.target; const formData = new FormData(form); try { const res = await fetch(`/api/items/${id}/media`, { method: "POST", body: formData, }); if (res.ok) { location.reload(); } else { const data = await res.json(); alert(data.error || "Failed to replace media"); } } catch (err) { console.error("Replace media error:", err); alert("Failed to replace media"); } return false; } // Delete item async function deleteItem(id) { if (!confirm("Delete this item?")) return; try { const res = await fetch(`/api/items/${id}`, { method: "DELETE" }); if (res.ok) { location.href = "/"; } else { const data = await res.json(); alert(data.error || "Failed to delete"); } } catch (err) { console.error("Delete error:", err); alert("Failed to delete"); } } // Play video (initial click to start) function playVideo(overlay) { const container = overlay.parentElement; const video = container.querySelector("video"); if (video) { video.play(); overlay.remove(); // Show controls on hover video.addEventListener("mouseenter", () => (video.controls = true)); video.addEventListener("mouseleave", () => (video.controls = false)); } } // Utility function escapeHtml(str) { const div = document.createElement("div"); div.textContent = str; return div.innerHTML; }