Files
homefeed/frontend/src/lib/components/admin/BookmarksTab.svelte
T
Claude ac154322f8 Add 1/2/3-column layout option to the Bookmarks widget
The bookmarks list can now render as 1 (default), 2, or 3 columns
instead of always being a single-column list, admin-configurable via
a pill picker on the Widgets tab. Column count is a new bookmarksColumns
field on GlobalSettings (same idiom as the other scalar admin knobs)
and rides along in the public GET /api/bookmarks response so the
sidebar picks it up without a separate request.
2026-08-02 00:00:40 +00:00

239 lines
5.9 KiB
Svelte

<script lang="ts">
import type { AdminBookmark } from '$lib/adminTypes';
import { addBookmark, updateBookmark, deleteBookmark, updateSettings } from '$lib/adminApi';
let { bookmarks: initial, columns: initialColumns }: { bookmarks: AdminBookmark[]; columns: 1 | 2 | 3 } = $props();
let bookmarks = $state([...initial]);
let columns = $state(initialColumns);
let showAdd = $state(false);
let newBookmark = $state({ name: '', url: '', isPrivate: false });
async function setColumns(n: 1 | 2 | 3) {
columns = n;
await updateSettings({ bookmarksColumns: n });
}
const columnOptions: (1 | 2 | 3)[] = [1, 2, 3];
let editingId = $state<string | null>(null);
let editForm = $state({ name: '', url: '' });
async function handleAdd() {
if (!newBookmark.name.trim() || !newBookmark.url.trim()) return;
const created = await addBookmark(newBookmark.name.trim(), newBookmark.url.trim(), newBookmark.isPrivate);
bookmarks = [...bookmarks, created];
newBookmark = { name: '', url: '', isPrivate: false };
showAdd = false;
}
async function togglePrivate(bookmark: AdminBookmark) {
const updated = await updateBookmark(bookmark.id, { isPrivate: !bookmark.isPrivate });
bookmarks = bookmarks.map((b) => (b.id === bookmark.id ? updated : b));
}
async function handleDelete(id: string) {
await deleteBookmark(id);
bookmarks = bookmarks.filter((b) => b.id !== id);
}
function startEdit(bookmark: AdminBookmark) {
editingId = bookmark.id;
editForm = { name: bookmark.name, url: bookmark.url };
}
function cancelEdit() {
editingId = null;
}
async function saveEdit() {
if (!editingId || !editForm.name.trim() || !editForm.url.trim()) return;
const updated = await updateBookmark(editingId, { name: editForm.name.trim(), url: editForm.url.trim() });
bookmarks = bookmarks.map((b) => (b.id === editingId ? updated : b));
editingId = null;
}
</script>
<div class="toolbar">
<span class="count">{bookmarks.length} bookmarks</span>
<div class="columns-picker">
<span class="field-label">Columns</span>
<div class="pill-row">
{#each columnOptions as n}
<button class="pill" class:active={columns === n} onclick={() => setColumns(n)}>{n}</button>
{/each}
</div>
</div>
<button class="add-btn" onclick={() => (showAdd = !showAdd)}>+ New bookmark</button>
</div>
{#if showAdd}
<div class="add-panel">
<div class="add-grid">
<input placeholder="Name, e.g. Weather.gov" bind:value={newBookmark.name} />
<input placeholder="https://example.com" bind:value={newBookmark.url} />
</div>
<label class="private-toggle">
<input type="checkbox" bind:checked={newBookmark.isPrivate} />
Private
</label>
<div class="add-actions">
<button onclick={() => (showAdd = false)}>Cancel</button>
<button class="primary" onclick={handleAdd}>Create</button>
</div>
</div>
{/if}
<div class="list">
{#each bookmarks as bookmark (bookmark.id)}
{#if editingId === bookmark.id}
<div class="edit-panel">
<div class="add-grid">
<input placeholder="Name" bind:value={editForm.name} />
<input placeholder="URL" bind:value={editForm.url} />
</div>
<div class="add-actions">
<button onclick={cancelEdit}>Cancel</button>
<button class="primary" onclick={saveEdit}>Save</button>
</div>
</div>
{:else}
<div class="row">
<div>
<div class="name">{bookmark.name}</div>
<div class="sub">{bookmark.url}</div>
</div>
<label class="private-toggle">
<input type="checkbox" checked={bookmark.isPrivate} onchange={() => togglePrivate(bookmark)} />
Private
</label>
<button class="icon-btn" onclick={() => startEdit(bookmark)} title="Edit">Edit</button>
<button class="icon-btn danger" onclick={() => handleDelete(bookmark.id)} title="Delete"></button>
</div>
{/if}
{/each}
</div>
<style>
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.count {
font-size: 12px;
color: var(--text-muted);
}
.columns-picker {
display: flex;
align-items: center;
gap: 8px;
}
.field-label {
font-size: 11px;
color: var(--text-muted);
}
.pill-row {
display: flex;
gap: 6px;
}
.pill {
font-size: 12px;
padding: 4px 10px;
border-radius: var(--radius);
border: 0.5px solid var(--border);
background: var(--surface-2);
color: var(--text-secondary);
}
.pill.active {
background: var(--pill-bg);
color: var(--pill-text);
border-color: var(--pill-bg);
}
.add-btn {
font-size: 12px;
padding: 6px 12px;
}
.add-panel {
background: var(--surface-1);
border-radius: 12px;
padding: 14px;
margin-bottom: 14px;
}
.add-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
margin-bottom: 10px;
}
.add-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-bottom: 6px;
}
.primary {
background: var(--pill-bg);
color: var(--pill-text);
border-color: var(--pill-bg);
}
.private-toggle {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
margin-bottom: 10px;
}
.private-toggle input {
width: auto;
}
.list {
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: var(--surface-1);
border-radius: var(--radius);
padding: 10px 14px;
}
.name {
font-size: 13px;
font-weight: 500;
}
.sub {
font-size: 11px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 320px;
}
.icon-btn {
font-size: 12px;
padding: 3px 6px;
background: transparent;
border: none;
color: var(--text-secondary);
}
.icon-btn:hover {
color: var(--text-accent);
}
.icon-btn.danger:hover {
color: var(--text-danger);
}
.edit-panel {
background: var(--surface-1);
border-radius: 12px;
padding: 14px;
}
</style>