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