a tool for shared writing and social publishing
1import { useUIState } from "src/useUIState";
2import { BlockLayout, BlockProps } from "./Block";
3import { useMemo } from "react";
4import { AsyncValueInput } from "components/Input";
5import { focusElement } from "src/utils/focusElement";
6import { useEntitySetContext } from "components/EntitySetProvider";
7import { useEntity, useReplicache } from "src/replicache";
8import { v7 } from "uuid";
9import { elementId } from "src/utils/elementId";
10import { CloseTiny } from "components/Icons/CloseTiny";
11import { useLeafletPublicationData } from "components/PageSWRDataProvider";
12import {
13 PubLeafletBlocksPoll,
14 PubLeafletPagesLinearDocument,
15} from "lexicons/api";
16import { getDocumentPages } from "src/utils/normalizeRecords";
17import { ids } from "lexicons/api/lexicons";
18
19/**
20 * PublicationPollBlock is used for editing polls in publication documents.
21 * It allows adding/editing options when the poll hasn't been published yet,
22 * but disables adding new options once the poll record exists (indicated by pollUri).
23 */
24export const PublicationPollBlock = (
25 props: BlockProps & {
26 areYouSure?: boolean;
27 setAreYouSure?: (value: boolean) => void;
28 },
29) => {
30 let { data: publicationData } = useLeafletPublicationData();
31 let isSelected = useUIState((s) =>
32 s.selectedBlocks.find((b) => b.value === props.entityID),
33 );
34 // Check if this poll has been published in a publication document
35 const isPublished = useMemo(() => {
36 if (!normalizedDocument) return false;
37
38 const pages = getDocumentPages(normalizedDocument);
39 if (!pages) return false;
40
41 // Search through all pages and blocks to find if this poll entity has been published
42 for (const page of pages) {
43 if (page.$type === "pub.leaflet.pages.linearDocument") {
44 const linearPage = page as PubLeafletPagesLinearDocument.Main;
45 for (const blockWrapper of linearPage.blocks || []) {
46 if (blockWrapper.block?.$type === ids.PubLeafletBlocksPoll) {
47 const pollBlock = blockWrapper.block as PubLeafletBlocksPoll.Main;
48 // Check if this poll's rkey matches our entity ID
49 const rkey = pollBlock.pollRef.uri.split("/").pop();
50 if (rkey === props.entityID) {
51 return true;
52 }
53 }
54 }
55 }
56 }
57 return false;
58 }, [normalizedDocument, props.entityID]);
59
60 return (
61 <BlockLayout
62 className="poll flex flex-col gap-2"
63 hasBackground={"accent"}
64 isSelected={!!isSelected}
65 areYouSure={props.areYouSure}
66 setAreYouSure={props.setAreYouSure}
67 >
68 <EditPollForPublication
69 entityID={props.entityID}
70 isPublished={isPublished}
71 />
72 </BlockLayout>
73 );
74};
75
76const EditPollForPublication = (props: {
77 entityID: string;
78 isPublished: boolean;
79}) => {
80 let pollOptions = useEntity(props.entityID, "poll/options");
81 let { rep } = useReplicache();
82 let permission_set = useEntitySetContext();
83
84 return (
85 <>
86 {props.isPublished && (
87 <div className="text-sm italic text-tertiary">
88 This poll has been published. You can't edit the options.
89 </div>
90 )}
91
92 {pollOptions.length === 0 && !props.isPublished && (
93 <div className="text-center italic text-tertiary text-sm">
94 no options yet...
95 </div>
96 )}
97
98 {pollOptions.map((p) => (
99 <EditPollOptionForPublication
100 key={p.id}
101 entityID={p.data.value}
102 pollEntity={props.entityID}
103 disabled={props.isPublished}
104 canDelete={!props.isPublished}
105 />
106 ))}
107
108 {!props.isPublished && permission_set.permissions.write && (
109 <button
110 className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
111 onClick={async () => {
112 let pollOptionEntity = v7();
113 await rep?.mutate.addPollOption({
114 pollEntity: props.entityID,
115 pollOptionEntity,
116 pollOptionName: "",
117 permission_set: permission_set.set,
118 factID: v7(),
119 });
120
121 focusElement(
122 document.getElementById(
123 elementId.block(props.entityID).pollInput(pollOptionEntity),
124 ) as HTMLInputElement | null,
125 );
126 }}
127 >
128 Add an Option
129 </button>
130 )}
131 </>
132 );
133};
134
135const EditPollOptionForPublication = (props: {
136 entityID: string;
137 pollEntity: string;
138 disabled: boolean;
139 canDelete: boolean;
140}) => {
141 let { rep } = useReplicache();
142 let { permissions } = useEntitySetContext();
143 let optionName = useEntity(props.entityID, "poll-option/name")?.data.value;
144
145 return (
146 <div className="flex gap-2 items-center">
147 <AsyncValueInput
148 id={elementId.block(props.pollEntity).pollInput(props.entityID)}
149 type="text"
150 className="pollOptionInput w-full input-with-border"
151 placeholder="Option here..."
152 disabled={props.disabled || !permissions.write}
153 value={optionName || ""}
154 onChange={async (e) => {
155 await rep?.mutate.assertFact([
156 {
157 entity: props.entityID,
158 attribute: "poll-option/name",
159 data: { type: "string", value: e.currentTarget.value },
160 },
161 ]);
162 }}
163 onKeyDown={(e) => {
164 if (
165 props.canDelete &&
166 e.key === "Backspace" &&
167 !e.currentTarget.value
168 ) {
169 e.preventDefault();
170 rep?.mutate.removePollOption({ optionEntity: props.entityID });
171 }
172 }}
173 />
174
175 {permissions.write && props.canDelete && (
176 <button
177 tabIndex={-1}
178 className="text-accent-contrast"
179 onMouseDown={async () => {
180 await rep?.mutate.removePollOption({
181 optionEntity: props.entityID,
182 });
183 }}
184 >
185 <CloseTiny />
186 </button>
187 )}
188 </div>
189 );
190};