Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
11 kB · 350 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351"use client";import { Popover } from "components/Popover";
import { Color } from "react-aria-components";
import { LeafletBackgroundPicker, PageThemePickers,} from "./Pickers/PageThemePickers";import { PageWidthSetter } from "./Pickers/PageWidthSetter";import { useMemo, useState } from "react";import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache";import { Replicache } from "replicache";import { FilterAttributes } from "src/replicache/attributes";import { colorToString } from "components/ThemeManager/useColorAttribute";import { useEntitySetContext } from "components/EntitySetProvider";import { ActionButton } from "components/ActionBar/ActionButton";import { CheckboxChecked } from "components/Icons/CheckboxChecked";import { CheckboxEmpty } from "components/Icons/CheckboxEmpty";import { PaintSmall } from "components/Icons/PaintSmall";import { AccentPickers } from "./Pickers/AccentPickers";import { useLeafletPublicationData } from "components/PageSWRDataProvider";import { useIsMobile } from "src/hooks/isMobile";import { Toggle } from "components/Toggle";import { getFontConfig, getFontFamilyValue } from "src/fonts";
export type pickers = | "null" | "leaflet" | "page" | "accent-1" | "accent-2" | "text" | "highlight-1" | "highlight-2" | "highlight-3" | "page-background-image" | "page-width";
export function setColorAttribute( rep: Replicache<ReplicacheMutators> | null, entity: string,) { return (attribute: keyof FilterAttributes<{ type: "color" }>) => (color: Color) => rep?.mutate.assertFact({ entity, attribute, data: { type: "color", value: colorToString(color, "hsba") }, });}export const ThemePopover = (props: { entityID: string; home?: boolean }) => { let { rep } = useReplicache(); let { data: pub } = useLeafletPublicationData(); let isMobile = useIsMobile();
// 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. let permission = useEntitySetContext().permissions.write; let leafletBGImage = useEntity(props.entityID, "theme/background-image"); let leafletBGRepeat = useEntity( props.entityID, "theme/background-image-repeat", );
let [openPicker, setOpenPicker] = useState<pickers>( props.home === true ? "leaflet" : "null", ); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]);
if (!permission) return null; if (pub?.publications) return null;
return ( <> <Popover className="w-80 bg-white py-3!" arrowFill="#FFFFFF" asChild side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} trigger={<ActionButton icon={<PaintSmall />} label="Theme" />} > <ThemeSetterContent {...props} /> </Popover> </> );};
export const ThemeSetterContent = (props: { entityID: string; home?: boolean;}) => { let { rep } = useReplicache(); let { data: pub } = useLeafletPublicationData();
// 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. let permission = useEntitySetContext().permissions.write; let leafletBGImage = useEntity(props.entityID, "theme/background-image"); let leafletBGRepeat = useEntity( props.entityID, "theme/background-image-repeat", );
let [openPicker, setOpenPicker] = useState<pickers>( props.home === true ? "leaflet" : "null", ); let set = useMemo(() => { return setColorAttribute(rep, props.entityID); }, [rep, props.entityID]);
if (!permission) return null; if (pub?.publications) return null; return ( <div className="themeSetterContent flex flex-col w-full overflow-y-scroll no-scrollbar"> {!props.home && ( <PageWidthSetter entityID={props.entityID} thisPicker={"page-width"} openPicker={openPicker} setOpenPicker={setOpenPicker} closePicker={() => setOpenPicker("null")} /> )} <div className="themeBGLeaflet flex"> <div className={`bgPicker flex flex-col gap-0 -mb-[6px] z-10 w-full `}> <div className="bgPickerBody w-full flex flex-col gap-2 p-2 mt-1 border border-[#CCCCCC] rounded-md"> <LeafletBackgroundPicker entityID={props.entityID} openPicker={openPicker} setOpenPicker={setOpenPicker} /> </div>
<SectionArrow fill="white" stroke="#CCCCCC" className="ml-2 -mt-px" /> </div> </div>
<div onClick={(e) => { e.currentTarget === e.target && setOpenPicker("leaflet"); }} style={{ backgroundImage: leafletBGImage ? `url(${leafletBGImage.data.src})` : undefined, backgroundRepeat: leafletBGRepeat ? "repeat" : "no-repeat", backgroundPosition: "center", backgroundSize: !leafletBGRepeat ? "cover" : `calc(${leafletBGRepeat.data.value}px / 2 )`, }} className={`bg-bg-leaflet px-3 pt-4 pb-0 mb-2 flex flex-col gap-4 rounded-md border border-border`} > <PageThemePickers entityID={props.entityID} openPicker={openPicker} setOpenPicker={(pickers) => setOpenPicker(pickers)} home={props.home} /> <div className="flex flex-col -gap-[6px]"> <div className={`flex flex-col z-10 -mb-[6px] `}> <AccentPickers entityID={props.entityID} openPicker={openPicker} setOpenPicker={(pickers) => setOpenPicker(pickers)} /> <SectionArrow fill="rgb(var(--accent-2))" stroke="rgb(var(--accent-1))" className="ml-2" /> </div>
<SampleButton entityID={props.entityID} setOpenPicker={setOpenPicker} /> </div>
<SamplePage setOpenPicker={setOpenPicker} home={props.home} entityID={props.entityID} /> </div> {!props.home && <WatermarkSetter entityID={props.entityID} />} </div> );};
function WatermarkSetter(props: { entityID: string }) { let { rep } = useReplicache(); let checked = useEntity(props.entityID, "theme/page-leaflet-watermark");
function handleToggle() { rep?.mutate.assertFact({ entity: props.entityID, attribute: "theme/page-leaflet-watermark", data: { type: "boolean", value: !checked?.data.value }, }); } return ( <div className="flex gap-2 items-start mt-0.5"> <Toggle toggle={!!checked?.data.value} onToggle={() => { handleToggle(); }} disabledColor1="#8C8C8C" disabledColor2="#DBDBDB" > <div className="flex flex-col gap-0 items-start "> <div className="font-bold">Show Leaflet Watermark</div> <div className="text-sm text-[#969696]">Help us spread the word!</div> </div> </Toggle> </div> );}
const SampleButton = (props: { entityID: string; setOpenPicker: (thisPicker: pickers) => void;}) => { return ( <div onClick={(e) => { e.target === e.currentTarget && props.setOpenPicker("accent-1"); }} 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" > <div className="cursor-pointer w-fit" onClick={() => { props.setOpenPicker("accent-2"); }} > Example Button </div> </div> );};const SamplePage = (props: { entityID: string; home: boolean | undefined; setOpenPicker: (picker: "page" | "text") => void;}) => { let pageBGImage = useEntity(props.entityID, "theme/card-background-image"); let pageBGRepeat = useEntity( props.entityID, "theme/card-background-image-repeat", ); let pageBGOpacity = useEntity( props.entityID, "theme/card-background-image-opacity", ); let pageBorderHidden = useEntity(props.entityID, "theme/card-border-hidden") ?.data.value;
// Read font values directly since the popover is portalled outside .leafletWrapper let headingFontId = useEntity(props.entityID, "theme/heading-font")?.data.value; let bodyFontId = useEntity(props.entityID, "theme/body-font")?.data.value; let bodyFontFamily = getFontFamilyValue(getFontConfig(bodyFontId)); let headingFontFamily = getFontFamilyValue(getFontConfig(headingFontId));
return ( <div onClick={(e) => { e.currentTarget === e.target && props.setOpenPicker("page"); }} className={` text-primary relative ${ pageBorderHidden ? "py-2 px-0 border border-transparent" : `cursor-pointer p-2 border border-border border-b-transparent shadow-md ${props.home ? "rounded-md " : "rounded-t-lg "}` }`} style={ pageBorderHidden ? undefined : { backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", } } > <div className="background absolute top-0 right-0 bottom-0 left-0 z-0 rounded-t-lg" style={ pageBorderHidden ? undefined : { backgroundImage: pageBGImage ? `url(${pageBGImage.data.src})` : undefined,
backgroundRepeat: pageBGRepeat ? "repeat" : "no-repeat", opacity: pageBGOpacity?.data.value || 1, backgroundSize: !pageBGRepeat ? "cover" : `calc(${pageBGRepeat.data.value}px / 2 )`, } } /> <div className="z-10 relative" style={{ fontFamily: bodyFontFamily }}> <p onClick={() => { props.setOpenPicker("text"); }} className="cursor-pointer font-bold w-fit" style={{ fontFamily: headingFontFamily }} > Hello! </p> <small onClick={() => props.setOpenPicker("text")}> Welcome to{" "} <span className="font-bold text-accent-contrast">Leaflet</span> — a fun and easy way to make, share, and collab on little bits of paper ✨ </small> </div> </div> );};
export const SectionArrow = (props: { fill: string; stroke: string; className: string;}) => { return ( <svg width="24" height="12" viewBox="0 0 24 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={props.className} > <path d="M11.9999 12L24 0H0L11.9999 12Z" fill={props.fill} /> <path fillRule="evenodd" clipRule="evenodd" d="M1.33552 0L12 10.6645L22.6645 0H24L12 12L0 0H1.33552Z" fill={props.stroke} /> </svg> );};