a tool for shared writing and social publishing
at main 350 lines 11 kB view raw
1"use client"; 2import { Popover } from "components/Popover"; 3 4import { Color } from "react-aria-components"; 5 6import { 7 LeafletBackgroundPicker, 8 PageThemePickers, 9} from "./Pickers/PageThemePickers"; 10import { PageWidthSetter } from "./Pickers/PageWidthSetter"; 11import { useMemo, useState } from "react"; 12import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache"; 13import { Replicache } from "replicache"; 14import { FilterAttributes } from "src/replicache/attributes"; 15import { colorToString } from "components/ThemeManager/useColorAttribute"; 16import { useEntitySetContext } from "components/EntitySetProvider"; 17import { ActionButton } from "components/ActionBar/ActionButton"; 18import { CheckboxChecked } from "components/Icons/CheckboxChecked"; 19import { CheckboxEmpty } from "components/Icons/CheckboxEmpty"; 20import { PaintSmall } from "components/Icons/PaintSmall"; 21import { AccentPickers } from "./Pickers/AccentPickers"; 22import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 23import { useIsMobile } from "src/hooks/isMobile"; 24import { Toggle } from "components/Toggle"; 25import { getFontConfig, getFontFamilyValue } from "src/fonts"; 26 27export type pickers = 28 | "null" 29 | "leaflet" 30 | "page" 31 | "accent-1" 32 | "accent-2" 33 | "text" 34 | "highlight-1" 35 | "highlight-2" 36 | "highlight-3" 37 | "page-background-image" 38 | "page-width"; 39 40export function setColorAttribute( 41 rep: Replicache<ReplicacheMutators> | null, 42 entity: string, 43) { 44 return (attribute: keyof FilterAttributes<{ type: "color" }>) => 45 (color: Color) => 46 rep?.mutate.assertFact({ 47 entity, 48 attribute, 49 data: { type: "color", value: colorToString(color, "hsba") }, 50 }); 51} 52export const ThemePopover = (props: { entityID: string; home?: boolean }) => { 53 let { rep } = useReplicache(); 54 let { data: pub } = useLeafletPublicationData(); 55 let isMobile = useIsMobile(); 56 57 // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. 58 let permission = useEntitySetContext().permissions.write; 59 let leafletBGImage = useEntity(props.entityID, "theme/background-image"); 60 let leafletBGRepeat = useEntity( 61 props.entityID, 62 "theme/background-image-repeat", 63 ); 64 65 let [openPicker, setOpenPicker] = useState<pickers>( 66 props.home === true ? "leaflet" : "null", 67 ); 68 let set = useMemo(() => { 69 return setColorAttribute(rep, props.entityID); 70 }, [rep, props.entityID]); 71 72 if (!permission) return null; 73 if (pub?.publications) return null; 74 75 return ( 76 <> 77 <Popover 78 className="w-80 bg-white py-3!" 79 arrowFill="#FFFFFF" 80 asChild 81 side={isMobile ? "top" : "right"} 82 align={isMobile ? "center" : "start"} 83 trigger={<ActionButton icon={<PaintSmall />} label="Theme" />} 84 > 85 <ThemeSetterContent {...props} /> 86 </Popover> 87 </> 88 ); 89}; 90 91export const ThemeSetterContent = (props: { 92 entityID: string; 93 home?: boolean; 94}) => { 95 let { rep } = useReplicache(); 96 let { data: pub } = useLeafletPublicationData(); 97 98 // I need to get these variables from replicache and then write them to the DB. I also need to parse them into a state that can be used here. 99 let permission = useEntitySetContext().permissions.write; 100 let leafletBGImage = useEntity(props.entityID, "theme/background-image"); 101 let leafletBGRepeat = useEntity( 102 props.entityID, 103 "theme/background-image-repeat", 104 ); 105 106 let [openPicker, setOpenPicker] = useState<pickers>( 107 props.home === true ? "leaflet" : "null", 108 ); 109 let set = useMemo(() => { 110 return setColorAttribute(rep, props.entityID); 111 }, [rep, props.entityID]); 112 113 if (!permission) return null; 114 if (pub?.publications) return null; 115 return ( 116 <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> 117 {!props.home && ( 118 <PageWidthSetter 119 entityID={props.entityID} 120 thisPicker={"page-width"} 121 openPicker={openPicker} 122 setOpenPicker={setOpenPicker} 123 closePicker={() => setOpenPicker("null")} 124 /> 125 )} 126 <div className="themeBGLeaflet flex"> 127 <div className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}> 128 <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md"> 129 <LeafletBackgroundPicker 130 entityID={props.entityID} 131 openPicker={openPicker} 132 setOpenPicker={setOpenPicker} 133 /> 134 </div> 135 136 <SectionArrow fill="white" stroke="#CCCCCC" className="ml-2 -mt-px" /> 137 </div> 138 </div> 139 140 <div 141 onClick={(e) => { 142 e.currentTarget === e.target && setOpenPicker("leaflet"); 143 }} 144 style={{ 145 backgroundImage: leafletBGImage 146 ? `url(${leafletBGImage.data.src})` 147 : undefined, 148 backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", 149 backgroundPosition: "center", 150 backgroundSize: !leafletBGRepeat 151 ? "cover" 152 : `calc(${leafletBGRepeat.data.value}px / 2 )`, 153 }} 154 className={`bg-bg-leaflet px-3 pt-4 pb-0 mb-2 flex flex-col gap-4 rounded-md border border-border`} 155 > 156 <PageThemePickers 157 entityID={props.entityID} 158 openPicker={openPicker} 159 setOpenPicker={(pickers) => setOpenPicker(pickers)} 160 home={props.home} 161 /> 162 <div className="flex flex-col -gap-[6px]"> 163 <div className={`flex flex-col z-10 -mb-[6px] `}> 164 <AccentPickers 165 entityID={props.entityID} 166 openPicker={openPicker} 167 setOpenPicker={(pickers) => setOpenPicker(pickers)} 168 /> 169 <SectionArrow 170 fill="rgb(var(--accent-2))" 171 stroke="rgb(var(--accent-1))" 172 className="ml-2" 173 /> 174 </div> 175 176 <SampleButton 177 entityID={props.entityID} 178 setOpenPicker={setOpenPicker} 179 /> 180 </div> 181 182 <SamplePage 183 setOpenPicker={setOpenPicker} 184 home={props.home} 185 entityID={props.entityID} 186 /> 187 </div> 188 {!props.home && <WatermarkSetter entityID={props.entityID} />} 189 </div> 190 ); 191}; 192 193function WatermarkSetter(props: { entityID: string }) { 194 let { rep } = useReplicache(); 195 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark"); 196 197 function handleToggle() { 198 rep?.mutate.assertFact({ 199 entity: props.entityID, 200 attribute: "theme/page-leaflet-watermark", 201 data: { type: "boolean", value: !checked?.data.value }, 202 }); 203 } 204 return ( 205 <div className="flex gap-2 items-start mt-0.5"> 206 <Toggle 207 toggle={!!checked?.data.value} 208 onToggle={() => { 209 handleToggle(); 210 }} 211 disabledColor1="#8C8C8C" 212 disabledColor2="#DBDBDB" 213 > 214 <div className="flex flex-col gap-0 items-start "> 215 <div className="font-bold">Show Leaflet Watermark</div> 216 <div className="text-sm text-[#969696]">Help us spread the word!</div> 217 </div> 218 </Toggle> 219 </div> 220 ); 221} 222 223const SampleButton = (props: { 224 entityID: string; 225 setOpenPicker: (thisPicker: pickers) => void; 226}) => { 227 return ( 228 <div 229 onClick={(e) => { 230 e.target === e.currentTarget && props.setOpenPicker("accent-1"); 231 }} 232 className="pointer-cursor font-bold relative text-center text-lg py-2 rounded-md bg-accent-1 text-accent-2 shadow-md flex items-center justify-center" 233 > 234 <div 235 className="cursor-pointer w-fit" 236 onClick={() => { 237 props.setOpenPicker("accent-2"); 238 }} 239 > 240 Example Button 241 </div> 242 </div> 243 ); 244}; 245const SamplePage = (props: { 246 entityID: string; 247 home: boolean | undefined; 248 setOpenPicker: (picker: "page" | "text") => void; 249}) => { 250 let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 251 let pageBGRepeat = useEntity( 252 props.entityID, 253 "theme/card-background-image-repeat", 254 ); 255 let pageBGOpacity = useEntity( 256 props.entityID, 257 "theme/card-background-image-opacity", 258 ); 259 let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden") 260 ?.data.value; 261 262 // Read font values directly since the popover is portalled outside .leafletWrapper 263 let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value; 264 let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value; 265 let bodyFontFamily = getFontFamilyValue(getFontConfig(bodyFontId)); 266 let headingFontFamily = getFontFamilyValue(getFontConfig(headingFontId)); 267 268 return ( 269 <div 270 onClick={(e) => { 271 e.currentTarget === e.target && props.setOpenPicker("page"); 272 }} 273 className={` 274 text-primary relative 275 ${ 276 pageBorderHidden 277 ? "py-2 px-0 border border-transparent" 278 : `cursor-pointer p-2 border border-border border-b-transparent shadow-md 279 ${props.home ? "rounded-md " : "rounded-t-lg "}` 280 }`} 281 style={ 282 pageBorderHidden 283 ? undefined 284 : { 285 backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 286 } 287 } 288 > 289 <div 290 className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 291 style={ 292 pageBorderHidden 293 ? undefined 294 : { 295 backgroundImage: pageBGImage 296 ? `url(${pageBGImage.data.src})` 297 : undefined, 298 299 backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 300 opacity: pageBGOpacity?.data.value || 1, 301 backgroundSize: !pageBGRepeat 302 ? "cover" 303 : `calc(${pageBGRepeat.data.value}px / 2 )`, 304 } 305 } 306 /> 307 <div className="z-10 relative" style={{ fontFamily: bodyFontFamily }}> 308 <p 309 onClick={() => { 310 props.setOpenPicker("text"); 311 }} 312 className="cursor-pointer font-bold w-fit" 313 style={{ fontFamily: headingFontFamily }} 314 > 315 Hello! 316 </p> 317 <small onClick={() => props.setOpenPicker("text")}> 318 Welcome to{" "} 319 <span className="font-bold text-accent-contrast">Leaflet</span> a 320 fun and easy way to make, share, and collab on little bits of paper 321 </small> 322 </div> 323 </div> 324 ); 325}; 326 327export const SectionArrow = (props: { 328 fill: string; 329 stroke: string; 330 className: string; 331}) => { 332 return ( 333 <svg 334 width="24" 335 height="12" 336 viewBox="0 0 24 12" 337 fill="none" 338 xmlns="http://www.w3.org/2000/svg" 339 className={props.className} 340 > 341 <path d="M11.9999 12L24 0H0L11.9999 12Z" fill={props.fill} /> 342 <path 343 fillRule="evenodd" 344 clipRule="evenodd" 345 d="M1.33552 0L12 10.6645L22.6645 0H24L12 12L0 0H1.33552Z" 346 fill={props.stroke} 347 /> 348 </svg> 349 ); 350};