/// import { action, computed, Default, equals, NAME, navigateTo, pattern, Stream, UI, type VNode, Writable, } from "commontools"; import ReadingItemDetail, { type ItemStatus, type ItemType, type ReadingItemPiece, } from "./reading-item-detail.tsx"; // Re-export types for consumers and tests export type { ItemStatus, ItemType, ReadingItemPiece }; interface ReadingListInput { items?: Writable>; } interface ReadingListOutput { [NAME]: string; [UI]: VNode; items: ReadingItemPiece[]; mentionable: ReadingItemPiece[]; totalCount: number; currentFilter: ItemStatus | "all"; filteredItems: ReadingItemPiece[]; filteredCount: number; summary: string; addItem: Stream<{ title: string; author: string; type: ItemType }>; removeItem: Stream<{ item: ReadingItemPiece }>; setFilter: Stream<{ status: ItemStatus | "all" }>; updateItem: Stream<{ item: ReadingItemPiece; status?: ItemStatus; rating?: number | null; notes?: string; }>; } const TYPE_EMOJI: Record = { book: "📚", article: "📄", paper: "📑", video: "🎬", }; // Pure helper functions - can be called directly in JSX const getTypeEmoji = (t: ItemType): string => TYPE_EMOJI[t] || "📄"; const renderStars = (rating: number | null): string => { if (!rating) return ""; return "★".repeat(rating) + "☆".repeat(5 - rating); }; export default pattern(({ items }) => { const filterStatus = Writable.of("all"); const newTitle = Writable.of(""); const newAuthor = Writable.of(""); const newType = Writable.of("article"); // Pattern-body actions - preferred for single-use handlers const addItem = action( ( { title, author, type }: { title: string; author: string; type: ItemType; }, ) => { const trimmedTitle = title.trim(); if (trimmedTitle) { // Create a new ReadingItemDetail piece and store its reference const newItemPiece = ReadingItemDetail({ title: trimmedTitle, author: author.trim(), type, addedAt: Date.now(), }); items.push(newItemPiece); newTitle.set(""); newAuthor.set(""); } }, ); const removeItem = action(({ item }: { item: ReadingItemPiece }) => { const current = items.get(); const index = current.findIndex((el) => equals(item, el)); if (index >= 0) { items.set(current.toSpliced(index, 1)); } }); const setFilter = action(({ status }: { status: ItemStatus | "all" }) => { filterStatus.set(status); }); const updateItem = action( ({ item, status, rating, notes, }: { item: ReadingItemPiece; status?: ItemStatus; rating?: number | null; notes?: string; }) => { // Use the item's actions to update properties if (status !== undefined) item.setStatus.send({ status }); if (rating !== undefined) item.setRating.send({ rating }); if (notes !== undefined) item.setNotes.send({ notes }); }, ); // Computed values const totalCount = computed(() => items.get().length); const filteredItems = computed((): ReadingItemPiece[] => { const status = filterStatus.get(); const allItems = items.get(); if (status === "all") { return [...allItems]; } return allItems.filter((item) => item && item.status === status); }); const filteredCount = computed(() => filteredItems.length); const summary = computed(() => { return items.get() .filter((item) => item) .map((item) => `${item.title ?? ""} (${item.status ?? ""})`) .join(", "); }); // For empty state display const hasNoFilteredItems = computed(() => filteredCount === 0); // Expose current filter as a computed (read-only) const currentFilter = computed(() => filterStatus.get()); return { [NAME]: computed(() => `Reading List (${items.get().length})`), [UI]: ( Reading List ({totalCount}) All Want Reading Done Dropped {computed(() => { return filteredItems.map((item: ReadingItemPiece) => ( {getTypeEmoji(item.type)} {item.title || "(untitled)"} {item.author && ( by {item.author} )} {item.status} {renderStars(item.rating) && ( {renderStars(item.rating)} )} {item.notes && ( {item.notes} )} navigateTo(item)} > Edit removeItem.send({ item })} > × )); })} {hasNoFilteredItems ? (
No items yet. Add something to read!
) : null}
addItem.send({ title: newTitle.get(), author: newAuthor.get(), type: newType.get(), })} > Add
), items, mentionable: items, totalCount, currentFilter, filteredItems, filteredCount, summary, addItem, removeItem, setFilter, updateItem, }; });