a tool for shared writing and social publishing
at feature/recommend 241 lines 7.6 kB view raw
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, BlockLayout } 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 let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value; 28 29 if (!url) { 30 if (!permissions.write) return null; 31 return <ButtonBlockSettings {...props} />; 32 } 33 34 return ( 35 <BlockLayout 36 isSelected={!!isSelected} 37 borderOnHover 38 hasAlignment={alignment !== "justify"} 39 className={`p-0! rounded-md! border-none!`} 40 > 41 <a 42 href={url?.data.value} 43 target="_blank" 44 className={` ${alignment === "justify" ? "w-full" : "w-fit"}`} 45 > 46 <ButtonPrimary 47 role="link" 48 type="submit" 49 fullWidth={alignment === "justify"} 50 > 51 {text?.data.value} 52 </ButtonPrimary> 53 </a> 54 </BlockLayout> 55 ); 56}; 57 58const ButtonBlockSettings = (props: BlockProps) => { 59 let { rep } = useReplicache(); 60 let smoker = useSmoker(); 61 let entity_set = useEntitySetContext(); 62 63 let isSelected = useUIState((s) => 64 s.selectedBlocks.find((b) => b.value === props.entityID), 65 ); 66 67 let [textValue, setTextValue] = useState(""); 68 let [urlValue, setUrlValue] = useState(""); 69 let text = textValue; 70 let url = urlValue; 71 let alignment = useEntity(props.entityID, "block/text-alignment")?.data.value; 72 73 let submit = async () => { 74 let entity = props.entityID; 75 if (!entity) { 76 entity = v7(); 77 await rep?.mutate.addBlock({ 78 permission_set: entity_set.set, 79 factID: v7(), 80 parent: props.parent, 81 type: "card", 82 position: generateKeyBetween(props.position, props.nextPosition), 83 newEntityID: entity, 84 }); 85 } 86 87 // if no valid url prefix, default to https 88 if ( 89 !urlValue.startsWith("http") && 90 !urlValue.startsWith("mailto") && 91 !urlValue.startsWith("tel:") 92 ) 93 url = `https://${urlValue}`; 94 95 // these mutations = simpler subset of addLinkBlock 96 if (!rep) return; 97 await rep.mutate.assertFact({ 98 entity: entity, 99 attribute: "block/type", 100 data: { type: "block-type-union", value: "button" }, 101 }); 102 await rep?.mutate.assertFact({ 103 entity: entity, 104 attribute: "button/text", 105 data: { 106 type: "string", 107 value: text, 108 }, 109 }); 110 await rep?.mutate.assertFact({ 111 entity: entity, 112 attribute: "button/url", 113 data: { 114 type: "string", 115 value: url, 116 }, 117 }); 118 }; 119 120 return ( 121 <div 122 className={`buttonBlockSettingsWrapper flex flex-col gap-2 w-full 123 `} 124 > 125 <ButtonPrimary 126 className={`relative ${ 127 alignment === "center" 128 ? "place-self-center" 129 : alignment === "left" 130 ? "place-self-start" 131 : alignment === "right" 132 ? "place-self-end" 133 : "place-self-center" 134 }`} 135 fullWidth={alignment === "justify"} 136 > 137 {text !== "" ? text : "Button"} 138 </ButtonPrimary> 139 <BlockLayout 140 isSelected={!!isSelected} 141 borderOnHover 142 hasBackground="accent" 143 className="buttonBlockSettings text-tertiar hover:cursor-pointer border-dashed! p-0!" 144 > 145 <form 146 className={`w-full`} 147 onSubmit={(e) => { 148 e.preventDefault(); 149 let rect = document 150 .getElementById("button-block-settings") 151 ?.getBoundingClientRect(); 152 if (!textValue) { 153 smoker({ 154 error: true, 155 text: "missing button text!", 156 position: { 157 y: rect ? rect.top : 0, 158 x: rect ? rect.left + 12 : 0, 159 }, 160 }); 161 return; 162 } 163 if (!urlValue) { 164 smoker({ 165 error: true, 166 text: "missing url!", 167 position: { 168 y: rect ? rect.top : 0, 169 x: rect ? rect.left + 12 : 0, 170 }, 171 }); 172 return; 173 } 174 if (!isUrl(urlValue)) { 175 smoker({ 176 error: true, 177 text: "invalid url!", 178 position: { 179 y: rect ? rect.top : 0, 180 x: rect ? rect.left + 12 : 0, 181 }, 182 }); 183 return; 184 } 185 submit(); 186 }} 187 > 188 <div className="buttonBlockSettingsContent w-full flex flex-col sm:flex-row gap-2 text-secondary px-2 py-3 sm:pb-3 pb-1"> 189 <div className="buttonBlockSettingsTitleInput flex gap-2 w-full sm:w-52"> 190 <BlockButtonSmall 191 className={`shrink-0 ${isSelected ? "text-tertiary" : "text-border"} `} 192 /> 193 <Separator /> 194 <Input 195 type="text" 196 className="w-full grow border-none outline-hidden bg-transparent" 197 placeholder="button text" 198 value={textValue} 199 onChange={(e) => setTextValue(e.target.value)} 200 onKeyDown={(e) => { 201 if ( 202 e.key === "Backspace" && 203 !e.currentTarget.value && 204 urlValue !== "" 205 ) 206 e.preventDefault(); 207 }} 208 /> 209 </div> 210 <div className="buttonBlockSettingsLinkInput grow flex gap-2 w-full"> 211 <LinkSmall 212 className={`shrink-0 ${isSelected ? "text-tertiary" : "text-border"} `} 213 /> 214 <Separator /> 215 <Input 216 type="text" 217 id="button-block-url-input" 218 className="w-full grow border-none outline-hidden bg-transparent" 219 placeholder="www.example.com" 220 value={urlValue} 221 onChange={(e) => setUrlValue(e.target.value)} 222 onKeyDown={(e) => { 223 if (e.key === "Backspace" && !e.currentTarget.value) 224 e.preventDefault(); 225 }} 226 /> 227 </div> 228 <button 229 id="button-block-settings" 230 type="submit" 231 className={`p-1 shrink-0 w-fit flex gap-2 items-center place-self-end ${isSelected ? "text-accent-contrast" : "text-accent-contrast sm:text-border"}`} 232 > 233 <div className="sm:hidden block">Save</div> 234 <CheckTiny /> 235 </button> 236 </div> 237 </form> 238 </BlockLayout> 239 </div> 240 ); 241};