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