a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Feature/per card linking (#86)

* added back to leaflet icon, componentized share button, added share option ot the page menu

* added some conditions to homebutton

* wire up per page linking!

* fix type error

* render back button on readonly sub page links

* clarify copy for page share links

---------

Co-authored-by: celine <celine@hyperlink.academy>
Co-authored-by: Brendan Schlagel <brendan.schlagel@gmail.com>

authored by

Jared Pereira
celine
Brendan Schlagel
and committed by
GitHub
21417b38 bd430786

+214 -137
+22 -4
components/HomeButton.tsx
··· 1 1 import Link from "next/link"; 2 2 import { useEntitySetContext } from "./EntitySetProvider"; 3 - import { HomeSmall } from "./Icons"; 3 + import { BackToLeafletSmall, HomeSmall } from "./Icons"; 4 4 import { HoverButton } from "./Buttons"; 5 + import { useState } from "react"; 6 + import { useParams, useSearchParams } from "next/navigation"; 5 7 6 8 export function HomeButton() { 7 - let entity_set = useEntitySetContext(); 8 - if (!entity_set.permissions.write) return; 9 + let { permissions } = useEntitySetContext(); 10 + let searchParams = useSearchParams(); 11 + let params = useParams(); 12 + let isSubpage = !!searchParams.get("page"); 13 + 14 + if (isSubpage) 15 + return ( 16 + <Link href={`/${params.leaflet_id}`}> 17 + <HoverButton 18 + noLabelOnMobile 19 + icon={<BackToLeafletSmall />} 20 + label="See Full Leaflet" 21 + background="bg-accent-1" 22 + text="text-accent-2" 23 + /> 24 + </Link> 25 + ); 26 + if (!permissions.write) return null; 9 27 return ( 10 28 <Link href="/home"> 11 29 <HoverButton 12 30 noLabelOnMobile 13 - icon=<HomeSmall /> 31 + icon={<HomeSmall />} 14 32 label="Go Home" 15 33 background="bg-accent-1" 16 34 text="text-accent-2"
+20
components/Icons.tsx
··· 64 64 ); 65 65 }; 66 66 67 + export const BackToLeafletSmall = (props: Props) => { 68 + return ( 69 + <svg 70 + width="24" 71 + height="24" 72 + viewBox="0 0 24 24" 73 + fill="none" 74 + xmlns="http://www.w3.org/2000/svg" 75 + {...props} 76 + > 77 + <path 78 + fillRule="evenodd" 79 + clipRule="evenodd" 80 + d="M7.39839 6.47294C7.42343 6.49449 7.44895 6.5176 7.47526 6.54143C7.6955 6.74089 7.97206 6.99136 8.50198 6.81076C8.74022 6.72956 8.93595 6.29174 9.18112 5.74335C9.33511 5.3989 9.5086 5.01083 9.72437 4.64011C10.5419 3.23546 12.0554 1.99715 13.0302 1.99715C13.8999 1.99715 13.7397 2.59541 13.614 3.06513L13.614 3.06515C13.5823 3.18358 13.5528 3.29384 13.5425 3.38428C13.5389 3.41589 13.535 3.44641 13.5313 3.47574L13.5312 3.47581C13.4975 3.73868 13.4761 3.90528 13.8171 3.89669C14.2032 3.88696 14.7365 3.26895 15.1725 2.75313C15.7749 2.04061 16.4509 1.30692 17.4695 1.28009C18.1877 1.26117 18.3265 1.68783 18.2066 2.21266C18.1629 2.40437 18.1805 2.55434 18.3635 2.67135C18.4676 2.73796 18.7735 2.66031 19.1138 2.57394C19.3624 2.51084 19.6294 2.44308 19.8494 2.42353C20.984 2.32271 21.5455 2.44997 21.8839 2.96087C22.1619 3.3805 21.7001 3.91926 21.342 4.3372L21.3417 4.33747C21.3166 4.36676 21.292 4.39547 21.2682 4.4235C20.9605 4.78591 20.664 5.13503 20.7996 5.46126C20.9101 5.72743 21.1371 5.87738 21.3597 6.02438C21.3972 6.04918 21.4359 6.07356 21.4749 6.09816L21.475 6.09819C21.8143 6.31203 22.1781 6.54124 22.0087 7.18702C21.8089 7.94841 20.9761 8.43005 19.8494 8.62334C19.5714 8.67102 19.2733 8.69772 18.9857 8.72346C18.2968 8.78515 17.6686 8.8414 17.5243 9.16777C17.3959 9.45791 17.5495 9.60493 17.7881 9.7563C18.0732 9.9372 18.5552 10.2546 18.3749 10.7743C18.3283 10.9085 18.2599 11.0341 18.1728 11.1508C17.3266 10.7655 16.3863 10.551 15.3959 10.551C12.1794 10.551 9.49153 12.8142 8.83792 15.8351C8.57047 15.8387 8.3012 15.8398 8.03257 15.841L7.73842 15.8424C6.89053 15.847 6.25964 15.9185 5.57086 15.9987C5.49225 16.0079 5.42251 16.0521 5.38131 16.1183C4.74752 17.137 4.32815 18.1792 3.84868 19.3744C3.74335 19.6369 3.63657 19.9031 3.52607 20.1726C3.38 20.5288 2.91044 20.8389 2.45949 20.6615C2.09581 20.5184 1.84461 20.022 1.99068 19.6657C2.53473 18.3389 3.16238 17.1187 3.84868 15.9987C4.96872 13.9782 7.1766 11.4993 9.5114 9.63708C11.5888 7.94281 13.5727 6.67465 15.8816 5.71236C16.1706 5.59191 16.0849 5.26081 15.7831 5.34623C13.8227 5.9012 12.0799 6.97873 10.2884 8.08638L10.0486 8.23457C8.00779 9.49493 6.29754 11.1993 5.24511 12.4385C5.05954 12.657 4.69463 12.4828 4.71217 12.1989C4.88857 9.34365 5.54205 7.4605 6.23357 6.7295C6.79469 6.13636 7.11476 6.22883 7.39839 6.47294ZM20.8557 17.2607C20.8557 20.2761 18.4113 22.7205 15.3959 22.7205C12.3806 22.7205 9.93616 20.2761 9.93616 17.2607C9.93616 14.2454 12.3806 11.801 15.3959 11.801C18.4113 11.801 20.8557 14.2454 20.8557 17.2607ZM15.9854 13.5028C16.2245 13.7518 16.2164 14.1474 15.9674 14.3865L13.6245 16.6357H18.7211C19.0662 16.6357 19.3461 16.9155 19.3461 17.2607C19.3461 17.6059 19.0662 17.8857 18.7211 17.8857H13.6244L15.9674 20.1349C16.2164 20.3739 16.2245 20.7696 15.9854 21.0186C15.7464 21.2676 15.3507 21.2757 15.1017 21.0366L11.6381 17.7115C11.5153 17.5937 11.4459 17.4309 11.4459 17.2607C11.4459 17.0905 11.5153 16.9277 11.6381 16.8098L15.1017 13.4847C15.3507 13.2457 15.7464 13.2538 15.9854 13.5028Z" 81 + fill="currentColor" 82 + /> 83 + </svg> 84 + ); 85 + }; 86 + 67 87 export const BlockSmall = (props: Props) => { 68 88 return ( 69 89 <svg
+5 -1
components/MobileFooter.tsx
··· 39 39 <ShareOptions rootEntity={props.entityID} /> 40 40 </div> 41 41 </div> 42 - ) : null} 42 + ) : ( 43 + <div className="pb-2 px-2 z-10"> 44 + <HomeButton /> 45 + </div> 46 + )} 43 47 </Media> 44 48 ); 45 49 }
+45 -29
components/Pages.tsx components/Pages/index.tsx
··· 1 1 "use client"; 2 2 import { useEffect, useState } from "react"; 3 3 import { useUIState } from "src/useUIState"; 4 - import { useEntitySetContext } from "./EntitySetProvider"; 4 + import { useEntitySetContext } from "../EntitySetProvider"; 5 5 import { useSearchParams } from "next/navigation"; 6 6 7 7 import { focusBlock } from "src/utils/focusBlock"; ··· 16 16 useReplicache, 17 17 } from "src/replicache"; 18 18 19 - import { Media } from "./Media"; 20 - import { DesktopPageFooter } from "./DesktopFooter"; 21 - import { ShareOptions } from "./ShareOptions"; 22 - import { ThemePopover } from "./ThemeManager/ThemeSetter"; 23 - import { HomeButton } from "./HomeButton"; 24 - import { Canvas } from "./Canvas"; 25 - import { DraftPostOptions } from "./Blocks/MailboxBlock"; 19 + import { Media } from "../Media"; 20 + import { DesktopPageFooter } from "../DesktopFooter"; 21 + import { ShareOptions } from "../ShareOptions"; 22 + import { ThemePopover } from "../ThemeManager/ThemeSetter"; 23 + import { HomeButton } from "../HomeButton"; 24 + import { Canvas } from "../Canvas"; 25 + import { DraftPostOptions } from "../Blocks/MailboxBlock"; 26 26 import { Blocks } from "components/Blocks"; 27 - import { MenuItem, Menu } from "./Layout"; 28 - import { MoreOptionsTiny, CloseTiny, PaintSmall } from "./Icons"; 29 - import { HelpPopover } from "./HelpPopover"; 27 + import { MenuItem, Menu } from "../Layout"; 28 + import { MoreOptionsTiny, CloseTiny, PaintSmall, ShareSmall } from "../Icons"; 29 + import { HelpPopover } from "../HelpPopover"; 30 30 import { CreateNewLeafletButton } from "app/home/CreateNewButton"; 31 31 import { scanIndex } from "src/replicache/utils"; 32 - import { PageThemeSetter } from "./ThemeManager/PageThemeSetter"; 33 - import { CardThemeProvider } from "./ThemeManager/ThemeProvider"; 32 + import { PageThemeSetter } from "../ThemeManager/PageThemeSetter"; 33 + import { CardThemeProvider } from "../ThemeManager/ThemeProvider"; 34 + import { PageShareMenu } from "./PageShareMenu"; 34 35 35 36 export function Pages(props: { rootPage: string }) { 36 37 let rootPage = useEntity(props.rootPage, "root/page")[0]; 37 - let firstPage = rootPage?.data.value || props.rootPage; 38 - let openPages = useUIState((s) => s.openPages); 38 + let pages = useUIState((s) => s.openPages); 39 39 let params = useSearchParams(); 40 - let openPage = params.get("openPage"); 41 - let pages = [...openPages]; 42 - if (openPage && !pages.includes(openPage)) pages.push(openPage); 40 + let queryRoot = params.get("page"); 41 + let firstPage = queryRoot || rootPage?.data.value || props.rootPage; 43 42 let entity_set = useEntitySetContext(); 44 43 45 44 return ( ··· 57 56 e.currentTarget === e.target && blurPage(); 58 57 }} 59 58 > 60 - {entity_set.permissions.write ? ( 61 - <Media mobile={false} className="h-full"> 62 - <div className="flex flex-col h-full justify-between mr-4 mt-1"> 59 + <Media mobile={false} className="h-full"> 60 + <div className="flex flex-col h-full justify-between mr-4 mt-1"> 61 + {entity_set.permissions.write ? ( 63 62 <div className="flex flex-col justify-center gap-2 "> 64 63 <ShareOptions rootEntity={props.rootPage} /> 65 64 <LeafletOptions entityID={props.rootPage} /> ··· 68 67 <hr className="text-border my-3" /> 69 68 <HomeButton /> 70 69 </div> 71 - </div> 72 - </Media> 73 - ) : null} 70 + ) : ( 71 + <div> 72 + {" "} 73 + <HomeButton />{" "} 74 + </div> 75 + )} 76 + </div> 77 + </Media> 74 78 </div> 75 79 <div className="flex items-stretch"> 76 80 <CardThemeProvider entityID={firstPage}> ··· 250 254 <CloseTiny /> 251 255 </button> 252 256 )} 253 - {<OptionsMenu entityID={props.entityID} />} 257 + {<OptionsMenu entityID={props.entityID} first={!!props.first} />} 254 258 </div> 255 259 ); 256 260 }; 257 261 258 - const OptionsMenu = (props: { entityID: string }) => { 259 - let [state, setState] = useState<"normal" | "theme">("normal"); 262 + const OptionsMenu = (props: { entityID: string; first: boolean }) => { 263 + let [state, setState] = useState<"normal" | "theme" | "share">("normal"); 260 264 let { permissions } = useEntitySetContext(); 261 265 if (!permissions.write) return null; 262 266 return ( ··· 280 284 > 281 285 {state === "normal" ? ( 282 286 <> 287 + {!props.first && ( 288 + <MenuItem 289 + onSelect={(e) => { 290 + e.preventDefault(); 291 + setState("share"); 292 + }} 293 + > 294 + <ShareSmall /> Share Page 295 + </MenuItem> 296 + )} 283 297 <MenuItem 284 298 onSelect={(e) => { 285 299 e.preventDefault(); ··· 289 303 <PaintSmall /> Theme Page 290 304 </MenuItem> 291 305 </> 292 - ) : ( 306 + ) : state === "theme" ? ( 293 307 <PageThemeSetter entityID={props.entityID} /> 294 - )} 308 + ) : state === "share" ? ( 309 + <PageShareMenu entityID={props.entityID} /> 310 + ) : null} 295 311 </Menu> 296 312 ); 297 313 };
+31
components/Pages/PageShareMenu.tsx
··· 1 + import { ShareButton, usePublishLink } from "components/ShareOptions"; 2 + import { useEffect, useState } from "react"; 3 + 4 + export const PageShareMenu = (props: { entityID: string }) => { 5 + let publishLink = usePublishLink(); 6 + let [collabLink, setCollabLink] = useState<null | string>(null); 7 + useEffect(() => { 8 + setCollabLink(window.location.pathname); 9 + }, []); 10 + 11 + return ( 12 + <div> 13 + <ShareButton 14 + text="Publish this Page" 15 + subtext="Share a read-only link to this page" 16 + helptext="🚨 recipients can view the entire Leaflet" 17 + smokerText="Publish link copied!" 18 + id="get-page-publish-link" 19 + link={`${publishLink}?page=${props.entityID}`} 20 + /> 21 + <ShareButton 22 + text="Collab on this Page" 23 + subtext="Invite people to edit this page" 24 + helptext="🚨 recipients can edit the entire Leaflet" 25 + smokerText="Collab link copied!" 26 + id="get-page-collab-link" 27 + link={`${collabLink}?page=${props.entityID}`} 28 + /> 29 + </div> 30 + ); 31 + };
+91 -70
components/ShareOptions/index.tsx
··· 6 6 import { useSmoker } from "components/Toast"; 7 7 import { Menu, MenuItem } from "components/Layout"; 8 8 import { HoverButton } from "components/Buttons"; 9 + import useSWR from "swr"; 9 10 11 + export let usePublishLink = () => { 12 + let { permission_token, rootEntity } = useReplicache(); 13 + let entity_set = useEntitySetContext(); 14 + let { data: publishLink } = useSWR( 15 + "publishLink-" + permission_token.id, 16 + async () => { 17 + if ( 18 + !permission_token.permission_token_rights.find( 19 + (s) => s.entity_set === entity_set.set && s.create_token, 20 + ) 21 + ) 22 + return; 23 + let shareLink = await getShareLink( 24 + { id: permission_token.id, entity_set: entity_set.set }, 25 + rootEntity, 26 + ); 27 + return shareLink?.id; 28 + }, 29 + ); 30 + return publishLink; 31 + }; 10 32 export function ShareOptions(props: { rootEntity: string }) { 11 33 let { permission_token } = useReplicache(); 12 34 let entity_set = useEntitySetContext(); 13 - let [link, setLink] = useState<null | string>(null); 35 + let publishLink = usePublishLink(); 36 + let [collabLink, setCollabLink] = useState<null | string>(null); 14 37 useEffect(() => { 15 - if ( 16 - !permission_token.permission_token_rights.find( 17 - (s) => s.entity_set === entity_set.set && s.create_token, 18 - ) 19 - ) 20 - return; 21 - getShareLink( 22 - { id: permission_token.id, entity_set: entity_set.set }, 23 - props.rootEntity, 24 - ).then((link) => { 25 - setLink(link?.id || null); 26 - }); 27 - }, [entity_set, permission_token, props.rootEntity]); 38 + setCollabLink(window.location.pathname); 39 + }, []); 40 + 28 41 let smoker = useSmoker(); 29 42 30 43 if ( ··· 45 58 /> 46 59 } 47 60 > 48 - <MenuItem 61 + <ShareButton 62 + text="Publish" 63 + subtext="Share a read-only version" 64 + smokerText="Publish link copied!" 49 65 id="get-publish-link" 50 - onSelect={(event) => { 51 - event.preventDefault(); 52 - let rect = document 53 - .getElementById("get-publish-link") 54 - ?.getBoundingClientRect(); 55 - if (link) { 56 - navigator.clipboard.writeText( 57 - `${location.protocol}//${location.host}/${link}`, 58 - ); 59 - smoker({ 60 - position: { 61 - x: rect ? rect.left + 80 : 0, 62 - y: rect ? rect.top + 26 : 0, 63 - }, 64 - text: "Publish link copied!", 65 - }); 66 - } 67 - }} 68 - > 69 - <div className="group/publish"> 70 - <div className=" group-hover/publish:text-accent-contrast"> 71 - Publish 72 - </div> 73 - <div className="text-sm font-normal text-tertiary group-hover/publish:text-accent-contrast"> 74 - Share a read only version of this leaflet 75 - </div> 76 - </div> 77 - </MenuItem> 78 - <MenuItem 66 + link={publishLink || ""} 67 + /> 68 + <ShareButton 69 + text="Collaborate" 70 + subtext="Invite people to edit together" 71 + smokerText="Collab link copied!" 79 72 id="get-collab-link" 80 - onSelect={(event) => { 81 - event.preventDefault(); 82 - let rect = document 83 - .getElementById("get-collab-link") 84 - ?.getBoundingClientRect(); 85 - if (link) { 86 - navigator.clipboard.writeText(`${window.location.href}`); 87 - smoker({ 88 - position: { 89 - x: rect ? rect.left + 80 : 0, 90 - y: rect ? rect.top + 26 : 0, 91 - }, 92 - text: "Collab link copied!", 93 - }); 94 - } 95 - }} 96 - > 97 - <div className="group/collab"> 98 - <div className="group-hover/collab:text-accent-contrast"> 99 - Collaborate 100 - </div> 101 - <div className="text-sm font-normal text-tertiary group-hover/collab:text-accent-contrast"> 102 - Invite people to work together 103 - </div> 104 - </div> 105 - </MenuItem>{" "} 73 + link={collabLink} 74 + /> 106 75 </Menu> 107 76 ); 108 77 } 78 + 79 + export const ShareButton = (props: { 80 + text: string; 81 + subtext: string; 82 + helptext?: string; 83 + smokerText: string; 84 + id: string; 85 + link: null | string; 86 + }) => { 87 + let smoker = useSmoker(); 88 + 89 + return ( 90 + <MenuItem 91 + id={props.id} 92 + onSelect={(e) => { 93 + e.preventDefault(); 94 + let rect = document.getElementById(props.id)?.getBoundingClientRect(); 95 + if (props.link) { 96 + navigator.clipboard.writeText( 97 + `${location.protocol}//${location.host}/${props.link}`, 98 + ); 99 + smoker({ 100 + position: { 101 + x: rect ? rect.left + 80 : 0, 102 + y: rect ? rect.top + 26 : 0, 103 + }, 104 + text: props.smokerText, 105 + }); 106 + } 107 + }} 108 + > 109 + <div className={`group/${props.id}`}> 110 + <div className={`group-hover/${props.id}:text-accent-contrast`}> 111 + {props.text} 112 + </div> 113 + <div 114 + className={`text-sm font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`} 115 + > 116 + {props.subtext} 117 + </div> 118 + {/* optional help text */} 119 + {props.helptext && ( 120 + <div 121 + className={`text-sm italic font-normal text-tertiary group-hover/${props.id}:text-accent-contrast`} 122 + > 123 + {props.helptext} 124 + </div> 125 + )} 126 + </div> 127 + </MenuItem> 128 + ); 129 + };
-33
components/ThemeManager/ThemeSetter.tsx
··· 237 237 setOpenPicker={setOpenPicker} 238 238 closePicker={() => setOpenPicker("null")} 239 239 /> 240 - {/* UNCOMMENT WHEN WE WIRE UP BG IMAGES ON FIRST PAGE */} 241 - {/* {(pageBGImage === null || !pageBGImage) && ( 242 - <label 243 - className={`m-0 h-max w-full py-0.5 px-1 244 - bg-accent-1 outline-transparent 245 - rounded-md text-base font-bold text-accent-2 246 - hover:cursor-pointer 247 - flex gap-2 items-center justify-center shrink-0 248 - transparent-outline hover:outline-accent-1 outline-offset-1 249 - `} 250 - > 251 - <BlockImageSmall /> Add Background Image 252 - <div className="hidden"> 253 - <ImageInput 254 - entityID={props.entityID} 255 - onChange={() => setOpenPicker("page-background-image")} 256 - card 257 - /> 258 - </div> 259 - </label> 260 - )} 261 - </ColorPicker> 262 - {pageBGImage && pageBGImage !== null && ( 263 - <PageBGPicker 264 - entityID={props.entityID} 265 - thisPicker={"page-background-image"} 266 - openPicker={openPicker} 267 - setOpenPicker={setOpenPicker} 268 - closePicker={() => setOpenPicker("null")} 269 - setValue={set("theme/card-background")} 270 - /> 271 - )} */} 272 - 273 240 <ColorPicker 274 241 label="Text" 275 242 value={primaryValue}