/// /** * Gift Preferences Module - Pattern for gift giving preferences * * A composable pattern that can be used standalone or embedded in containers * like Record. Tracks gift tier, favorites, and items to avoid. */ import { Cell, computed, type Default, handler, NAME, recipe, UI, } from "commontools"; import type { ModuleMetadata } from "./container-protocol.ts"; // ===== Self-Describing Metadata ===== export const MODULE_METADATA: ModuleMetadata = { type: "giftprefs", label: "Gift Prefs", icon: "\u{1F381}", // gift emoji schema: { giftTier: { type: "string", enum: ["", "always", "occasions", "reciprocal", "none"], description: "Gift giving tier", }, favorites: { type: "array", items: { type: "string" }, description: "Favorite things", }, avoid: { type: "array", items: { type: "string" }, description: "Things to avoid", }, }, fieldMapping: ["giftTier", "favorites", "avoid"], }; // ===== Types ===== /** Gift giving tier (always=give often, occasions=holidays/birthdays, reciprocal=if they give, none=don't give) */ type GiftTier = "always" | "occasions" | "reciprocal" | "none"; export interface GiftPrefsModuleInput { /** Gift giving tier */ giftTier: Default; /** Favorite things (interests, hobbies, brands) */ favorites: Default; /** Things to avoid (allergies, dislikes) */ avoid: Default; } // ===== Constants ===== const TIER_OPTIONS = [ { value: "", label: "Not set" }, { value: "always", label: "🎁 Always (exchange gifts)" }, { value: "occasions", label: "🎂 Occasions only" }, { value: "reciprocal", label: "↔️ Reciprocal (if they give)" }, { value: "none", label: "⛔ None (no gift exchange)" }, ]; // ===== Handlers ===== // Handler to add a favorite const addFavorite = handler< unknown, { favorites: Cell; favoriteInput: Cell } >((_event, { favorites, favoriteInput }) => { const newItem = favoriteInput.get().trim(); if (!newItem) return; const current = favorites.get() || []; if (!current.includes(newItem)) { favorites.set([...current, newItem]); } favoriteInput.set(""); }); // Handler to remove a favorite by index const removeFavorite = handler< unknown, { favorites: Cell; index: number } >((_event, { favorites, index }) => { favorites.set((favorites.get() || []).toSpliced(index, 1)); }); // Handler to add an avoid item const addAvoid = handler< unknown, { avoid: Cell; avoidInput: Cell } >((_event, { avoid, avoidInput }) => { const newItem = avoidInput.get().trim(); if (!newItem) return; const current = avoid.get() || []; if (!current.includes(newItem)) { avoid.set([...current, newItem]); } avoidInput.set(""); }); // Handler to remove an avoid item by index const removeAvoid = handler< unknown, { avoid: Cell; index: number } >((_event, { avoid, index }) => { avoid.set((avoid.get() || []).toSpliced(index, 1)); }); // ===== The Pattern ===== export const GiftPrefsModule = recipe< GiftPrefsModuleInput, GiftPrefsModuleInput >( "GiftPrefsModule", ({ giftTier, favorites, avoid }) => { const favoriteInput = Cell.of(""); const avoidInput = Cell.of(""); const displayText = computed(() => { const opt = TIER_OPTIONS.find((o) => o.value === giftTier); return opt?.label || "Not set"; }); return { [NAME]: computed(() => `${MODULE_METADATA.icon} Gift Prefs: ${displayText}` ), [UI]: ( {/* Gift tier */} {/* Favorites */} + {favorites.map((item: string, index: number) => ( {item} ))} {/* Avoid */} + {avoid.map((item: string, index: number) => ( {item} ))} ), giftTier, favorites, avoid, }; }, ); export default GiftPrefsModule;