The Cards Manager

Dated Apr 28, 2024; last modified on Sun, 28 Apr 2024

UI Design

Legacy card viewing UI at /browse.

Legacy card viewing UI at /browse.

This time we’ll use the more semantic and a11y-friendly <dialog> element . Centering it in the page is done by the browser, and that saves us a bit of hassle – thought it would have been feasible with how <search-bar>’s <ul> floats above the page.

CardsManager Interface

The previous/next buttons make use of the CardsManager object that has the API:

interface MiniCard { _id: string; urgency: number, tags: string[] }
interface TagsAndIds { [tag: string]: { [id: string]: { urgency: number }} }

class CardsManager {
  constructor(tagsAndIds: TagsAndIds, userID: string, cardSourceURL="/read-card", minicards: MiniCard[] = []);
  function initializeFromTags(tagsToUse: Set<string> | null): Promise<void>;
  function initializeFromMinicards(minicards: MiniCard[], includeTagNeighbors=false): Promise<void>;
  function initializeFromTrash(cardIds: string[]): Promise<void>;
  function fetchCard(cardId: string): Promise<Card | null>;
  function next(): Promise<Card | null>;
  function hasNext(): Boolean;
  function previous(): Promise<Card | null>;
  function hasPrevious(): Boolean;
  function removeCard(cardId: string): string;
  function status(): void;
  function insertCard(cardId: string, urgency: number): void;
  function updateCard(card: Card): void;
  function quartiles(): [number, number, number, number, number];
  function saveCard(card: Card, url: string);
}

Internally, the CardsManager uses ’s AVL tree which is available both in a Node and in a browser environment.

At its core, the CardsManager maintains the queue of cards that are accessible using the previous and next buttons. The card itself is responsible for actions like persisting its content on the server. We can use the “events go up; properties come down” design principle to have a card announce changes in its content, and the CardsManager can listen for such updates and updates its state accordingly.

The CardsManager should therefore be at the top-level web component. It can be availed via Context to descendants that need it, e.g., the previous and next buttons.

Having 4 ways of initializing a CardsManager seems excessive. The current initialization paths are used as:

// Browse page
cardsManager = new CardsManager(tagsAndIDs, publicUserID, '/read-public-card');
cardsManager.initializeFromMinicards([{ _id: cardIdToShow, urgency, tags}], includeTagNeighbors=true);

// Home
cardsManager = new CardsManager(tagsAndIDs, currentUserID, '/read-card', mini_cards);
cardsManager.initializeFromTags(selectedTags || new Set([]));
cardsManager.initializeFromMinicards([clickedSearchResult]);

// Account page
cardsManager = new CardsManager(metadata.node_information[0], currentUserID);
cardsManager.initializeFromTrash(state.metadata.trashed_cards[0]);

The fact that we’re using tRPC means that we can replace cardSourceURL with cardSearchEndpoint. CardsManager queries the endpoint in fetchCard, next, previous, and status. However, this is not strictly needed. For instance, in the web-component based /browse, the <browse-page> component queries the endpoint in response to a search-result-selected message. Similarly, the next/previous buttons can fire this event, and have the <browse-page> load the appropriate card, and pass it as a property to the public-card-viewer.

The userId parameter is also not needed. The endpoints themselves enforce the appropriate user ID, e.g., fetchCard enforces the ID that is in the session object.

CardsManager.initializeFromTags is not needed anymore, given that we are doing away with the tags bar on the left. Selecting cards that match a given tag will be done via the search box, which will bubble up the CardSearchResult[] via the search-results event to the searchResultsContext.

CardsManager.initializeFromMinicards is a weird one. /home passes includeTagNeighbors=false, the CardsManager only has 1 card in the queue, which is somewhat less useful. That said, this case is still covered by the search-results event. /browse passes includeTagNeighbors=true, which makes the CardsManager also add cards that share a tag with the card that is being shown based on the TagsAndIds passed in the ctor. This can be computed instead by <browse-page> and passed in CardsManager’s ctor as a MiniCard[].

CardsManager.initializeFromTrash should be removed from /account and in its place, the user can access deleted cards from <search-results>.

With the above changes, CardsManager can be initialized through the simpler constructor(miniCards: MiniCard[]) that also captures that CardsManager deals with the set of cards that the user can see. It would also be beneficial to rename the class to something CardsCarousel instead of the CardsManager which promises more functionality than is provided.

References

  1. `<dialog>`: The Dialog element - HTML: HyperText Markup Language | MDN. developer.mozilla.org . Accessed Apr 28, 2024.
  2. w8r/avl: :eyeglasses: Fast AVL tree for Node and browser. github.com . Accessed Apr 28, 2024.