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