a tool for shared writing and social publishing
at feature/recommend 340 lines 10 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 /> 160 <div className="flex flex-col -gap-[6px]"> 161 <div className={`flex flex-col z-10 -mb-[6px] `}> 162 <AccentPickers 163 entityID={props.entityID} 164 openPicker={openPicker} 165 setOpenPicker={(pickers) => setOpenPicker(pickers)} 166 /> 167 <SectionArrow 168 fill="rgb(var(--accent-2))" 169 stroke="rgb(var(--accent-1))" 170 className="ml-2" 171 /> 172 </div> 173 174 <SampleButton 175 entityID={props.entityID} 176 setOpenPicker={setOpenPicker} 177 /> 178 </div> 179 180 <SamplePage 181 setOpenPicker={setOpenPicker} 182 home={props.home} 183 entityID={props.entityID} 184 /> 185 </div> 186 {!props.home && <WatermarkSetter entityID={props.entityID} />} 187 </div> 188 ); 189}; 190function WatermarkSetter(props: { entityID: string }) { 191 let { rep } = useReplicache(); 192 let checked = useEntity(props.entityID, "theme/page-leaflet-watermark"); 193 194 function handleToggle() { 195 rep?.mutate.assertFact({ 196 entity: props.entityID, 197 attribute: "theme/page-leaflet-watermark", 198 data: { type: "boolean", value: !checked?.data.value }, 199 }); 200 } 201 return ( 202 <div className="flex gap-2 items-start mt-0.5"> 203 <Toggle 204 toggle={!!checked?.data.value} 205 onToggle={() => { 206 handleToggle(); 207 }} 208 disabledColor1="#8C8C8C" 209 disabledColor2="#DBDBDB" 210 > 211 <div className="flex flex-col gap-0 items-start "> 212 <div className="font-bold">Show Leaflet Watermark</div> 213 <div className="text-sm text-[#969696]">Help us spread the word!</div> 214 </div> 215 </Toggle> 216 </div> 217 ); 218} 219 220const SampleButton = (props: { 221 entityID: string; 222 setOpenPicker: (thisPicker: pickers) => void; 223}) => { 224 return ( 225 <div 226 onClick={(e) => { 227 e.target === e.currentTarget && props.setOpenPicker("accent-1"); 228 }} 229 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" 230 > 231 <div 232 className="cursor-pointer w-fit" 233 onClick={() => { 234 props.setOpenPicker("accent-2"); 235 }} 236 > 237 Example Button 238 </div> 239 </div> 240 ); 241}; 242const SamplePage = (props: { 243 entityID: string; 244 home: boolean | undefined; 245 setOpenPicker: (picker: "page" | "text") => void; 246}) => { 247 let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); 248 let pageBGRepeat = useEntity( 249 props.entityID, 250 "theme/card-background-image-repeat", 251 ); 252 let pageBGOpacity = useEntity( 253 props.entityID, 254 "theme/card-background-image-opacity", 255 ); 256 let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden") 257 ?.data.value; 258 259 return ( 260 <div 261 onClick={(e) => { 262 e.currentTarget === e.target && props.setOpenPicker("page"); 263 }} 264 className={` 265 text-primary relative 266 ${ 267 pageBorderHidden 268 ? "py-2 px-0 border border-transparent" 269 : `cursor-pointer p-2 border border-border border-b-transparent shadow-md 270 ${props.home ? "rounded-md " : "rounded-t-lg "}` 271 }`} 272 style={ 273 pageBorderHidden 274 ? undefined 275 : { 276 backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 277 } 278 } 279 > 280 <div 281 className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" 282 style={ 283 pageBorderHidden 284 ? undefined 285 : { 286 backgroundImage: pageBGImage 287 ? `url(${pageBGImage.data.src})` 288 : undefined, 289 290 backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", 291 opacity: pageBGOpacity?.data.value || 1, 292 backgroundSize: !pageBGRepeat 293 ? "cover" 294 : `calc(${pageBGRepeat.data.value}px / 2 )`, 295 } 296 } 297 /> 298 <div className="z-10 relative"> 299 <p 300 onClick={() => { 301 props.setOpenPicker("text"); 302 }} 303 className="cursor-pointer font-bold w-fit" 304 > 305 Hello! 306 </p> 307 <small onClick={() => props.setOpenPicker("text")}> 308 Welcome to{" "} 309 <span className="font-bold text-accent-contrast">Leaflet</span> a 310 fun and easy way to make, share, and collab on little bits of paper 311 </small> 312 </div> 313 </div> 314 ); 315}; 316 317export const SectionArrow = (props: { 318 fill: string; 319 stroke: string; 320 className: string; 321}) => { 322 return ( 323 <svg 324 width="24" 325 height="12" 326 viewBox="0 0 24 12" 327 fill="none" 328 xmlns="http://www.w3.org/2000/svg" 329 className={props.className} 330 > 331 <path d="M11.9999 12L24 0H0L11.9999 12Z" fill={props.fill} /> 332 <path 333 fillRule="evenodd" 334 clipRule="evenodd" 335 d="M1.33552 0L12 10.6645L22.6645 0H24L12 12L0 0H1.33552Z" 336 fill={props.stroke} 337 /> 338 </svg> 339 ); 340};