a tool for shared writing and social publishing

create invite contributors page

+213 -23
+7
app/globals.css
··· 377 377 @apply rounded-md; 378 378 } 379 379 380 + .page-container { 381 + background: rgba(var(--bg-page), var(--bg-page-alpha)); 382 + @apply border; 383 + @apply border-bg-page; 384 + @apply rounded-md; 385 + } 386 + 380 387 .container { 381 388 background: rgba(var(--bg-page), 0.75); 382 389 @apply border;
+13
app/lish/[did]/[publication]/PubNotFound.tsx
··· 1 + import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 2 + 3 + export const PubNotFound = () => { 4 + return ( 5 + <NotFoundLayout> 6 + <p className="font-bold">Sorry, we can't find this publication!</p> 7 + <p> 8 + This may be a glitch on our end. If the issue persists please{" "} 9 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 10 + </p> 11 + </NotFoundLayout> 12 + ); 13 + };
+2 -2
app/lish/[did]/[publication]/dashboard/publicationSettings/PublicationContributors.tsx
··· 40 40 const PubContributorsEmpty = () => { 41 41 return ( 42 42 <div className="flex flex-col gap-2 justify-center accent-container text-sm text-center sm:p-4 p-3 mb-1 mt-3"> 43 - <PubContibutorEmptyIllo /> 43 + <PubContributorEmptyIllo /> 44 44 <div className="font-bold"> 45 45 Contributors can make drafts and publish in this publication!{" "} 46 46 </div> ··· 135 135 ); 136 136 }; 137 137 138 - const PubContibutorEmptyIllo = () => { 138 + export const PubContributorEmptyIllo = () => { 139 139 return ( 140 140 <svg 141 141 width="52"
+172
app/lish/[did]/[publication]/invite-contributor/page.tsx
··· 1 + import { BskyAgent } from "@atproto/api"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { 4 + PublicationThemeProvider, 5 + PublicationBackgroundProvider, 6 + } from "components/ThemeManager/PublicationThemeProvider"; 7 + import { PubLeafletPublication } from "lexicons/api"; 8 + import { supabaseServerClient } from "supabase/serverClient"; 9 + import { ButtonPrimary } from "components/Buttons"; 10 + import { PubContributorEmptyIllo } from "../dashboard/publicationSettings/PublicationContributors"; 11 + import { PubNotFound } from "../PubNotFound"; 12 + import { PubIcon } from "components/ActionBar/Publications"; 13 + import { PubListing } from "app/(home-pages)/discover/PubListing"; 14 + import { SpeedyLink } from "components/SpeedyLink"; 15 + import { BlueskyLogin } from "app/login/LoginForm"; 16 + import { getIdentityData } from "actions/getIdentityData"; 17 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 18 + 19 + export default async function Publication(props: { 20 + params: Promise<{ publication: string; did: string }>; 21 + }) { 22 + let identity = await getIdentityData(); 23 + let params = await props.params; 24 + let did = decodeURIComponent(params.did); 25 + if (!did) return <PubNotFound />; 26 + let uri; 27 + let publication_name = decodeURIComponent(params.publication); 28 + if (/^(?!\.$|\.\.S)[A-Za-z0-9._:~-]{1,512}$/.test(publication_name)) { 29 + uri = AtUri.make( 30 + did, 31 + "pub.leaflet.publication", 32 + publication_name, 33 + ).toString(); 34 + } 35 + let [{ data: publication }] = await Promise.all([ 36 + supabaseServerClient 37 + .from("publications") 38 + .select( 39 + `*, 40 + publication_subscriptions(*), 41 + documents_in_publications(documents( 42 + *, 43 + comments_on_documents(count), 44 + document_mentions_in_bsky(count) 45 + )) 46 + `, 47 + ) 48 + .eq("identity_did", did) 49 + .or(`name.eq."${publication_name}", uri.eq."${uri}"`) 50 + .single(), 51 + ]); 52 + 53 + let pubRecord = publication?.record as PubLeafletPublication.Record; 54 + let showPageBackground = pubRecord?.theme?.showPageBackground; 55 + let pubUrl = getPublicationURL(publication!); 56 + let invited = false; 57 + 58 + if (!publication) return <PubNotFound />; 59 + 60 + return ( 61 + <PublicationThemeProvider 62 + record={pubRecord} 63 + pub_creator={publication.identity_did} 64 + > 65 + <PublicationBackgroundProvider 66 + record={pubRecord} 67 + pub_creator={publication.identity_did} 68 + > 69 + <div className={`flex h-full place-items-center`}> 70 + <div 71 + className={`${showPageBackground ? "page-container" : ""} sm:mx-auto mx-3 max-w-sm w-full p-3 sm:p-4 flex flex-col justify-center text-center`} 72 + > 73 + {!identity || !identity?.atp_did ? ( 74 + <LoggedOutContent 75 + pubRecord={pubRecord} 76 + uri={publication.uri} 77 + pubUrl={pubUrl} 78 + /> 79 + ) : invited ? ( 80 + <InvitedContent 81 + pubRecord={pubRecord} 82 + uri={publication.uri} 83 + pubUrl={pubUrl} 84 + /> 85 + ) : ( 86 + <NotInvitedContent 87 + pubRecord={pubRecord} 88 + uri={publication.uri} 89 + pubUrl={pubUrl} 90 + /> 91 + )} 92 + </div> 93 + </div> 94 + </PublicationBackgroundProvider> 95 + </PublicationThemeProvider> 96 + ); 97 + } 98 + 99 + const InvitedContent = (props: { 100 + pubRecord: PubLeafletPublication.Record; 101 + uri: string; 102 + pubUrl: string; 103 + }) => { 104 + return ( 105 + <> 106 + <h2>Become a Contributor!</h2> 107 + <PubLink {...props} /> 108 + <div> 109 + You've been invited to write for <br /> 110 + {props.pubRecord.name}!{" "} 111 + </div> 112 + <ButtonPrimary className="mx-auto mt-4 mb-2">Accept Invite</ButtonPrimary> 113 + </> 114 + ); 115 + }; 116 + 117 + const NotInvitedContent = (props: { 118 + pubRecord: PubLeafletPublication.Record; 119 + uri: string; 120 + pubUrl: string; 121 + }) => { 122 + return ( 123 + <> 124 + <h3 className="pb-2">You haven't been invited to contribute yet...</h3> 125 + <PubLink {...props} /> 126 + 127 + <div> 128 + If you are expecting an invite, please check that the owner of this 129 + publication added you to the invited contributors list. 130 + </div> 131 + </> 132 + ); 133 + }; 134 + 135 + const LoggedOutContent = (props: { 136 + pubRecord: PubLeafletPublication.Record; 137 + uri: string; 138 + pubUrl: string; 139 + }) => { 140 + return ( 141 + <> 142 + <h2>Log in to Contribute</h2> 143 + <PubLink {...props} /> 144 + <div className="pb-2"> 145 + Log in with an AT Proto handle to contribute this publication 146 + </div> 147 + <BlueskyLogin redirectRoute={`${props.pubUrl}/invite-contributor`} /> 148 + </> 149 + ); 150 + }; 151 + 152 + const PubLink = (props: { 153 + pubRecord: PubLeafletPublication.Record; 154 + uri: string; 155 + pubUrl: string; 156 + }) => { 157 + return ( 158 + <SpeedyLink 159 + href={props.pubUrl} 160 + className="p-4 flex flex-col justify-center text-center border border-border rounded-lg mt-2 mb-4 hover:no-underline! no-underline!" 161 + > 162 + <PubIcon 163 + large 164 + record={props.pubRecord} 165 + uri={props.uri} 166 + className="mx-auto mb-3 mt-1" 167 + /> 168 + <h3 className="leading-tight">{props.pubRecord.name}</h3> 169 + <div className="text-tertiary italic">{props.pubRecord.description}</div> 170 + </SpeedyLink> 171 + ); 172 + };
+1 -12
app/lish/[did]/[publication]/page.tsx
··· 16 16 import { CommentTiny } from "components/Icons/CommentTiny"; 17 17 import { LocalizedDate } from "./LocalizedDate"; 18 18 import { PublicationHomeLayout } from "./PublicationHomeLayout"; 19 + import { PubNotFound } from "./PubNotFound"; 19 20 20 21 export default async function Publication(props: { 21 22 params: Promise<{ publication: string; did: string }>; ··· 195 196 return <pre>{JSON.stringify(e, undefined, 2)}</pre>; 196 197 } 197 198 } 198 - 199 - const PubNotFound = () => { 200 - return ( 201 - <NotFoundLayout> 202 - <p className="font-bold">Sorry, we can't find this publication!</p> 203 - <p> 204 - This may be a glitch on our end. If the issue persists please{" "} 205 - <a href="mailto:contact@leaflet.pub">send us a note</a>. 206 - </p> 207 - </NotFoundLayout> 208 - ); 209 - };
+9 -9
components/Buttons.tsx
··· 35 35 m-0 h-max 36 36 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 37 37 ${compact ? "py-0 px-1" : "px-2 py-0.5 "} 38 - bg-accent-1 outline-transparent border border-accent-1 38 + bg-accent-1 border border-accent-1 39 39 rounded-md text-base font-bold text-accent-2 40 40 flex gap-2 items-center justify-center shrink-0 41 - transparent-outline focus:outline-accent-1 hover:outline-accent-1 outline-offset-1 41 + transparent-outline focus:outline-accent-1! hover:outline-accent-1! outline-offset-1 42 42 disabled:bg-border-light disabled:border-border-light disabled:text-border disabled:hover:text-border 43 43 ${className} 44 44 `} ··· 73 73 className={`m-0 h-max 74 74 ${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"} 75 75 ${props.compact ? "py-0 px-1" : "px-2 py-0.5 "} 76 - bg-bg-page outline-transparent 77 - rounded-md text-base font-bold text-accent-contrast 78 - flex gap-2 items-center justify-center shrink-0 79 - transparent-outline focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1 80 - border border-accent-contrast 81 - disabled:bg-border-light disabled:text-border disabled:hover:text-border 82 - ${props.className} 76 + bg-bg-page 77 + rounded-md text-base font-bold text-accent-contrast 78 + flex gap-2 items-center justify-center shrink-0 79 + transparent-outline focus:outline-accent-contrast! hover:outline-accent-contrast! outline-offset-1 80 + border border-accent-contrast 81 + disabled:bg-border-light disabled:text-border disabled:hover:text-border 82 + ${props.className} 83 83 `} 84 84 > 85 85 {props.children}
+9
components/PageLayouts/ModalLayout.tsx
··· 1 + export const ModalLayout = (props: { children: React.ReactNode }) => { 2 + return ( 3 + <div className="w-screen h-full flex place-items-center bg-bg-leaflet p-4 text-primary"> 4 + <div className="bg-bg-page mx-auto p-4 border border-border rounded-md flex flex-col text-center justify-center gap-1 w-fit"> 5 + {props.children} 6 + </div> 7 + </div> 8 + ); 9 + };