///
/**
* Bookmarks - A pattern for collecting and browsing URLs/bookmarks.
*
* Displays saved links in a searchable grid with rich previews using
* ct-link-preview. Users can add URLs, search across titles/descriptions/URLs,
* and remove bookmarks.
*
* Keywords: bookmarks, links, collection, urls, grid, preview, search
*/
import {
action,
computed,
Default,
NAME,
pattern,
UI,
type VNode,
Writable,
} from "commontools";
// ===== Types =====
interface Bookmark {
url: string;
title: Default;
description: Default;
}
interface BookmarksInput {
bookmarks?: Writable>;
}
interface BookmarksOutput {
[NAME]: string;
[UI]: VNode;
bookmarks: Bookmark[];
count: number;
}
// ===== The Pattern =====
export const Bookmarks = pattern(
({ bookmarks }) => {
const searchQuery = Writable.of("");
const filteredBookmarks = computed(() => {
const query = searchQuery.get().toLowerCase();
if (!query) return bookmarks.get() ?? [];
return (bookmarks.get() ?? []).filter((b) => {
const url = (b.url ?? "").toLowerCase();
const title = (b.title ?? "").toLowerCase();
const description = (b.description ?? "").toLowerCase();
return url.includes(query) || title.includes(query) ||
description.includes(query);
});
});
const count = computed(() => (bookmarks.get() ?? []).length);
const addBookmark = action(({ url }: { url: string }) => {
const trimmed = url.trim();
if (!trimmed) return;
bookmarks.push({ url: trimmed, title: "", description: "" });
});
const removeBookmark = action(({ index }: { index: number }) => {
const current = bookmarks.get() ?? [];
if (index < 0 || index >= current.length) return;
bookmarks.set(current.toSpliced(index, 1));
});
return {
[NAME]: computed(() => `🔖 Bookmarks (${count})`),
[UI]: (
{/* Add URL input */}
{
const url = e.detail?.message;
if (url) addBookmark.send({ url });
}}
/>
{/* Search */}
{/* Grid of link previews */}
{filteredBookmarks.map((bookmark: Bookmark, index: number) => (
))}
),
bookmarks,
count,
};
},
);
export default Bookmarks;