a tool for shared writing and social publishing
at debug/datetime 185 lines 5.9 kB view raw
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};