import { computed, NAME, pattern, type Stream, UI, type VNode, Writable, } from "commonfabric"; import type { CastVoteEvent, LogVisitEvent, Option, RemoveOptionEvent, Vote, VoteColor, } from "./main.tsx"; /** Shared per-session target cell used for one open option editor at a time. */ export type PollOptionLinkTargetCell = Writable; const myVoteFor = ( votes: readonly Vote[], me: string, optionId: string, ): VoteColor | undefined => { if (!me) return undefined; return votes.find( (v) => v.voterName === me && v.optionId === optionId, )?.voteType; }; /** * PollOptionCard renders one complete ranked restaurant option row. * * Use it when a parent pattern already owns option, vote, viewer, and admin * state and wants composed UI for voting and admin-only remove/history actions. * This is not a standalone vote engine; durable mutations happen through the * input streams supplied by the parent. */ /** * Inputs for one rendered ranked option row. * * The parent owns all durable and shared UI state. This pattern receives one * option, current viewer/admin facts, shared per-session editor state, and the * streams it should emit for mutations. When rendering inside `options.map()`, * pass the resolved `me` value from the parent, not the raw `myName` PerUser * cell. */ export interface PollOptionCardInput { /** Option record to render. */ option: Option; /** One-based display rank supplied by the parent's ranking computation. */ rank: number; /** Resolved current viewer name; required for per-option vote styling. */ me: string; /** Whether the current viewer is allowed to vote. */ isJoined: boolean; /** Whether the current viewer owns admin-only actions. */ isAdmin: boolean; /** Shared vote list used to compute this viewer's selected vote. */ votes: readonly Vote[]; /** Per-session option id awaiting admin remove confirmation. */ removeConfirmTarget: PollOptionLinkTargetCell; /** Parent-owned stream that toggles or records this viewer's vote. */ castVote: Stream; /** Parent-owned admin stream that removes this option after confirmation. */ removeOption: Stream; /** Parent-owned admin stream that records this option in visit history. */ logVisit: Stream; } /** * Outputs for one rendered ranked option row. * * Parents normally embed this sub-pattern with JSX. */ export interface PollOptionCardOutput { /** Human-readable pattern name, matching the option title. */ [NAME]: string; /** Static VNode rendering the complete option row. */ [UI]: VNode; } export default pattern( ( { option, rank, me, isJoined, isAdmin, votes, removeConfirmTarget, castVote, removeOption, logVisit, }, ) => { const oid = option.id; const optionTitle = option.title; const myVote = computed(() => myVoteFor(votes, me, oid)); const isRemoveConfirm = computed(() => removeConfirmTarget.get() === oid); return { [NAME]: optionTitle, [UI]: (
#{rank}
{optionTitle}
added by {option.addedByName} {isAdmin ? ( ) : null} {isAdmin ? ( ) : null}
{isRemoveConfirm ? (
Remove "{optionTitle}" and discard its votes? { removeOption.send({ optionId: oid }); removeConfirmTarget.set(null); }} > Yes, remove removeConfirmTarget.set(null)} > Cancel
) : null}
{isJoined ? (
castVote.send({ optionId: oid, voteType: "green", })} > ๐ŸŸข castVote.send({ optionId: oid, voteType: "yellow", })} > ๐ŸŸก castVote.send({ optionId: oid, voteType: "red", })} > ๐Ÿ”ด
) : null}
), }; }, );