///
/**
* 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;