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