a tool for shared writing and social publishing
1import { useEntitySetContext } from "components/EntitySetProvider"; 2import { generateKeyBetween } from "fractional-indexing"; 3import { useCallback, useEffect, useState } from "react"; 4import { useEntity, useReplicache } from "src/replicache"; 5import { useUIState } from "src/useUIState"; 6import { BlockProps } from "./Block"; 7import { v7 } from "uuid"; 8import { useSmoker } from "components/Toast"; 9 10import { Separator } from "components/Layout"; 11import { Input } from "components/Input"; 12import { isUrl } from "src/utils/isURL"; 13import { ButtonPrimary } from "components/Buttons"; 14import { BlockButtonSmall } from "components/Icons/BlockButtonSmall"; 15import { CheckTiny } from "components/Icons/CheckTiny"; 16import { LinkSmall } from "components/Icons/LinkSmall"; 17 18export const ButtonBlock = (props: BlockProps & { preview?: boolean }) => { 19 let { permissions } = useEntitySetContext(); 20 21 let text = useEntity(props.entityID, "button/text"); 22 let url = useEntity(props.entityID, "button/url"); 23 24 let isSelected = useUIState((s) => 25 s.selectedBlocks.find((b) => b.value === props.entityID), 26 ); 27 28 if (!url) { 29 if (!permissions.write) return null; 30 return <ButtonBlockSettings {...props} />; 31 } 32 33 return ( 34 <a 35 href={url?.data.value} 36 target="_blank" 37 className={`hover:outline-accent-contrast rounded-md! ${isSelected ? "block-border-selected border-0!" : "block-border border-transparent! border-0!"}`} 38 > 39 <ButtonPrimary role="link" type="submit"> 40 {text?.data.value} 41 </ButtonPrimary> 42 </a> 43 ); 44}; 45 46const ButtonBlockSettings = (props: BlockProps) => { 47 let { rep } = useReplicache(); 48 let smoker = useSmoker(); 49 let entity_set = useEntitySetContext(); 50 51 let isSelected = useUIState((s) => 52 s.selectedBlocks.find((b) => b.value === props.entityID), 53 ); 54 let isLocked = useEntity(props.entityID, "block/is-locked")?.data.value; 55 56 let [textValue, setTextValue] = useState(""); 57 let [urlValue, setUrlValue] = useState(""); 58 let text = textValue; 59 let url = urlValue; 60 61 let submit = async () => { 62 let entity = props.entityID; 63 if (!entity) { 64 entity = v7(); 65 await rep?.mutate.addBlock({ 66 permission_set: entity_set.set, 67 factID: v7(), 68 parent: props.parent, 69 type: "card", 70 position: generateKeyBetween(props.position, props.nextPosition), 71 newEntityID: entity, 72 }); 73 } 74 75 // if no valid url prefix, default to https 76 if ( 77 !urlValue.startsWith("http") && 78 !urlValue.startsWith("mailto") && 79 !urlValue.startsWith("tel:") 80 ) 81 url = `https://${urlValue}`; 82 83 // these mutations = simpler subset of addLinkBlock 84 if (!rep) return; 85 await rep.mutate.assertFact({ 86 entity: entity, 87 attribute: "block/type", 88 data: { type: "block-type-union", value: "button" }, 89 }); 90 await rep?.mutate.assertFact({ 91 entity: entity, 92 attribute: "button/text", 93 data: { 94 type: "string", 95 value: text, 96 }, 97 }); 98 await rep?.mutate.assertFact({ 99 entity: entity, 100 attribute: "button/url", 101 data: { 102 type: "string", 103 value: url, 104 }, 105 }); 106 }; 107 108 return ( 109 <div className="buttonBlockSettingsWrapper flex flex-col gap-2 w-full"> 110 <ButtonPrimary className="mx-auto"> 111 {text !== "" ? text : "Button"} 112 </ButtonPrimary> 113 114 <form 115 className={` 116 buttonBlockSettingsBorder 117 w-full bg-bg-page 118 text-tertiary hover:text-accent-contrast hover:cursor-pointer hover:p-0 119 flex flex-col gap-2 items-center justify-center hover:border-2 border-dashed rounded-lg 120 ${isSelected ? "border-2 border-tertiary p-0" : "border border-border p-px"} 121 `} 122 onSubmit={(e) => { 123 e.preventDefault(); 124 let rect = document 125 .getElementById("button-block-settings") 126 ?.getBoundingClientRect(); 127 if (!textValue) { 128 smoker({ 129 error: true, 130 text: "missing button text!", 131 position: { 132 y: rect ? rect.top : 0, 133 x: rect ? rect.left + 12 : 0, 134 }, 135 }); 136 return; 137 } 138 if (!urlValue) { 139 smoker({ 140 error: true, 141 text: "missing url!", 142 position: { 143 y: rect ? rect.top : 0, 144 x: rect ? rect.left + 12 : 0, 145 }, 146 }); 147 return; 148 } 149 if (!isUrl(urlValue)) { 150 smoker({ 151 error: true, 152 text: "invalid url!", 153 position: { 154 y: rect ? rect.top : 0, 155 x: rect ? rect.left + 12 : 0, 156 }, 157 }); 158 return; 159 } 160 submit(); 161 }} 162 > 163 <div className="buttonBlockSettingsContent w-full flex flex-col sm:flex-row gap-2 text-secondary px-2 py-3 sm:pb-3 pb-1"> 164 <div className="buttonBlockSettingsTitleInput flex gap-2 w-full sm:w-52"> 165 <BlockButtonSmall 166 className={`shrink-0 ${isSelected ? "text-tertiary" : "text-border"} `} 167 /> 168 <Separator /> 169 <Input 170 type="text" 171 autoFocus 172 className="w-full grow border-none outline-hidden bg-transparent" 173 placeholder="button text" 174 value={textValue} 175 disabled={isLocked} 176 onChange={(e) => setTextValue(e.target.value)} 177 onKeyDown={(e) => { 178 if ( 179 e.key === "Backspace" && 180 !e.currentTarget.value && 181 urlValue !== "" 182 ) 183 e.preventDefault(); 184 }} 185 /> 186 </div> 187 <div className="buttonBlockSettingsLinkInput grow flex gap-2 w-full"> 188 <LinkSmall 189 className={`shrink-0 ${isSelected ? "text-tertiary" : "text-border"} `} 190 /> 191 <Separator /> 192 <Input 193 type="text" 194 id="button-block-url-input" 195 className="w-full grow border-none outline-hidden bg-transparent" 196 placeholder="www.example.com" 197 value={urlValue} 198 disabled={isLocked} 199 onChange={(e) => setUrlValue(e.target.value)} 200 onKeyDown={(e) => { 201 if (e.key === "Backspace" && !e.currentTarget.value) 202 e.preventDefault(); 203 }} 204 /> 205 </div> 206 <button 207 id="button-block-settings" 208 type="submit" 209 className={`p-1 shrink-0 w-fit flex gap-2 items-center place-self-end ${isSelected && !isLocked ? "text-accent-contrast" : "text-accent-contrast sm:text-border"}`} 210 > 211 <div className="sm:hidden block">Save</div> 212 <CheckTiny /> 213 </button> 214 </div> 215 </form> 216 </div> 217 ); 218};