const API = '/api'; let state = { categories: [], activeCategoryId: null, subcategories: [], activeSubcategoryId: null, links: [] }; const globeSVG = ` `; async function api(method, path, body) { const opts = { method, headers: body ? { 'Content-Type': 'application/json' } : {} }; if (body) opts.body = JSON.stringify(body); const res = await fetch(API + path, opts); if (!res.ok) throw new Error(await res.text()); return res.json(); } const get = (path) => api('GET', path); const post = (path, body) => api('POST', path, body); const put = (path, body) => api('PUT', path, body); const del = (path) => api('DELETE', path); async function loadCategories() { state.categories = await get('/categories'); renderCategoryNav(); if (state.categories.length > 0) { await selectCategory( state.activeCategoryId || state.categories[0].id ); } } function renderCategoryNav() { const nav = document.getElementById('category-nav'); nav.innerHTML = ''; state.categories.forEach(cat => { const btn = document.createElement('button'); btn.className = 'cat-tab' + (cat.id === state.activeCategoryId ? ' active' : ''); btn.innerHTML = `${cat.name}`; btn.onclick = () => selectCategory(cat.id); btn.oncontextmenu = (e) => { e.preventDefault(); openEditCategoryModal(cat); }; nav.appendChild(btn); }); } async function selectCategory(id) { state.activeCategoryId = id; state.subcategories = await get('/subcategories/' + id); state.activeSubcategoryId = null; renderCategoryNav(); renderSubcategoryBar(); if (state.subcategories.length > 0) { await selectSubcategory(state.subcategories[0].id); } else { state.links = []; renderLinkGrid(); } } function renderSubcategoryBar() { const bar = document.getElementById('subcategory-bar'); bar.innerHTML = ''; state.subcategories.forEach(sub => { const btn = document.createElement('button'); btn.className = 'sub-tab' + (sub.id === state.activeSubcategoryId ? ' active' : ''); btn.textContent = sub.name; btn.onclick = () => selectSubcategory(sub.id); btn.oncontextmenu = (e) => { e.preventDefault(); openEditSubcategoryModal(sub); }; bar.appendChild(btn); }); const addBtn = document.createElement('button'); addBtn.className = 'btn-add-sub'; addBtn.innerHTML = ' Add subcategory'; addBtn.onclick = () => openAddSubcategoryModal(); bar.appendChild(addBtn); } async function selectSubcategory(id) { state.activeSubcategoryId = id; state.links = await get('/links/' + id); renderSubcategoryBar(); renderLinkGrid(); } function renderLinkGrid() { const grid = document.getElementById('link-grid'); grid.innerHTML = ''; state.links.forEach(link => { const a = document.createElement('a'); a.className = 'link-card'; a.href = link.url; a.target = '_blank'; a.rel = 'noopener noreferrer'; const favicon = document.createElement('div'); favicon.className = 'link-favicon'; if (link.favicon) { const img = document.createElement('img'); img.src = '/favicons/' + link.favicon; img.width = 20; img.height = 20; img.onerror = () => { favicon.innerHTML = globeSVG; }; favicon.appendChild(img); } else { favicon.innerHTML = globeSVG; } const label = document.createElement('span'); label.className = 'link-label'; label.textContent = link.label; const actions = document.createElement('div'); actions.className = 'link-actions'; actions.innerHTML = ` `; a.appendChild(favicon); a.appendChild(label); a.appendChild(actions); grid.appendChild(a); }); const addCard = document.createElement('button'); addCard.className = 'add-link-card'; addCard.innerHTML = ' Add link'; addCard.onclick = () => openAddLinkModal(); grid.appendChild(addCard); } function openModal(title, bodyHTML, onConfirm) { document.getElementById('modal-title').textContent = title; document.getElementById('modal-body').innerHTML = bodyHTML + ` `; document.getElementById('modal-overlay').classList.remove('hidden'); document.getElementById('modal-cancel').onclick = closeModal; document.getElementById('modal-close').onclick = closeModal; document.getElementById('modal-confirm').onclick = onConfirm; } function closeModal() { document.getElementById('modal-overlay').classList.add('hidden'); } function openAddCategoryModal() { openModal('Add category', `
`, async () => { const name = document.getElementById('f-name').value.trim(); const icon = document.getElementById('f-icon').value.trim(); if (!name) return; await post('/categories', { name, icon, position: state.categories.length }); closeModal(); await loadCategories(); }); } function openEditCategoryModal(cat) { openModal('Edit category', `
`, async () => { const name = document.getElementById('f-name').value.trim(); const icon = document.getElementById('f-icon').value.trim(); if (!name) return; await put('/categories/' + cat.id, { name, icon, position: cat.position }); closeModal(); await loadCategories(); }); } function openAddSubcategoryModal() { openModal('Add subcategory', `
`, async () => { const name = document.getElementById('f-name').value.trim(); if (!name) return; await post('/subcategories', { category_id: state.activeCategoryId, name, position: state.subcategories.length }); closeModal(); await selectCategory(state.activeCategoryId); }); } function openEditSubcategoryModal(sub) { openModal('Edit subcategory', `
`, async () => { const name = document.getElementById('f-name').value.trim(); if (!name) return; await put('/subcategories/' + sub.id, { category_id: state.activeCategoryId, name, position: sub.position }); closeModal(); await selectCategory(state.activeCategoryId); }); } function openAddLinkModal() { openModal('Add link', `
`, async () => { const label = document.getElementById('f-label').value.trim(); const url = document.getElementById('f-url').value.trim(); const favicon = document.getElementById('f-favicon').value.trim(); if (!label || !url) return; await post('/links', { subcategory_id: state.activeSubcategoryId, label, url, favicon: favicon || null, position: state.links.length }); closeModal(); await selectSubcategory(state.activeSubcategoryId); }); } function openEditLinkModal(link, e) { e.preventDefault(); e.stopPropagation(); openModal('Edit link', `
`, async () => { const label = document.getElementById('f-label').value.trim(); const url = document.getElementById('f-url').value.trim(); const favicon = document.getElementById('f-favicon').value.trim(); if (!label || !url) return; await put('/links/' + link.id, { subcategory_id: state.activeSubcategoryId, label, url, favicon: favicon || null, position: link.position }); closeModal(); await selectSubcategory(state.activeSubcategoryId); }); } async function deleteCategory(id) { closeModal(); await del('/categories/' + id); state.activeCategoryId = null; await loadCategories(); } async function deleteSubcategory(id) { closeModal(); await del('/subcategories/' + id); await selectCategory(state.activeCategoryId); } async function deleteLink(id, e) { e.preventDefault(); e.stopPropagation(); await del('/links/' + id); await selectSubcategory(state.activeSubcategoryId); } document.getElementById('search-form').addEventListener('submit', (e) => { e.preventDefault(); const q = document.getElementById('search-input').value.trim(); if (q) window.open('https://kagi.com/search?q=' + encodeURIComponent(q), '_blank'); }); document.getElementById('add-category-btn').onclick = openAddCategoryModal; document.getElementById('modal-overlay').addEventListener('click', (e) => { if (e.target === document.getElementById('modal-overlay')) closeModal(); }); loadCategories();