a tool for shared writing and social publishing
1import { useUIState } from "src/useUIState";
2import { 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 <div
57 className={`poll flex flex-col gap-2 p-3 w-full
58 ${isSelected ? "block-border-selected " : "block-border"}`}
59 style={{
60 backgroundColor:
61 "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)",
62 }}
63 >
64 <EditPollForPublication
65 entityID={props.entityID}
66 isPublished={isPublished}
67 />
68 </div>
69 );
70};
71
72const EditPollForPublication = (props: {
73 entityID: string;
74 isPublished: boolean;
75}) => {
76 let pollOptions = useEntity(props.entityID, "poll/options");
77 let { rep } = useReplicache();
78 let permission_set = useEntitySetContext();
79
80 return (
81 <>
82 {props.isPublished && (
83 <div className="text-sm italic text-tertiary">
84 This poll has been published. You can't edit the options.
85 </div>
86 )}
87
88 {pollOptions.length === 0 && !props.isPublished && (
89 <div className="text-center italic text-tertiary text-sm">
90 no options yet...
91 </div>
92 )}
93
94 {pollOptions.map((p) => (
95 <EditPollOptionForPublication
96 key={p.id}
97 entityID={p.data.value}
98 pollEntity={props.entityID}
99 disabled={props.isPublished}
100 canDelete={!props.isPublished}
101 />
102 ))}
103
104 {!props.isPublished && permission_set.permissions.write && (
105 <button
106 className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
107 onClick={async () => {
108 let pollOptionEntity = v7();
109 await rep?.mutate.addPollOption({
110 pollEntity: props.entityID,
111 pollOptionEntity,
112 pollOptionName: "",
113 permission_set: permission_set.set,
114 factID: v7(),
115 });
116
117 focusElement(
118 document.getElementById(
119 elementId.block(props.entityID).pollInput(pollOptionEntity),
120 ) as HTMLInputElement | null,
121 );
122 }}
123 >
124 Add an Option
125 </button>
126 )}
127 </>
128 );
129};
130
131const EditPollOptionForPublication = (props: {
132 entityID: string;
133 pollEntity: string;
134 disabled: boolean;
135 canDelete: boolean;
136}) => {
137 let { rep } = useReplicache();
138 let { permissions } = useEntitySetContext();
139 let optionName = useEntity(props.entityID, "poll-option/name")?.data.value;
140
141 return (
142 <div className="flex gap-2 items-center">
143 <AsyncValueInput
144 id={elementId.block(props.pollEntity).pollInput(props.entityID)}
145 type="text"
146 className="pollOptionInput w-full input-with-border"
147 placeholder="Option here..."
148 disabled={props.disabled || !permissions.write}
149 value={optionName || ""}
150 onChange={async (e) => {
151 await rep?.mutate.assertFact([
152 {
153 entity: props.entityID,
154 attribute: "poll-option/name",
155 data: { type: "string", value: e.currentTarget.value },
156 },
157 ]);
158 }}
159 onKeyDown={(e) => {
160 if (
161 props.canDelete &&
162 e.key === "Backspace" &&
163 !e.currentTarget.value
164 ) {
165 e.preventDefault();
166 rep?.mutate.removePollOption({ optionEntity: props.entityID });
167 }
168 }}
169 />
170
171 {permissions.write && props.canDelete && (
172 <button
173 tabIndex={-1}
174 className="text-accent-contrast"
175 onMouseDown={async () => {
176 await rep?.mutate.removePollOption({
177 optionEntity: props.entityID,
178 });
179 }}
180 >
181 <CloseTiny />
182 </button>
183 )}
184 </div>
185 );
186};