a tool for shared writing and social publishing

new subscribe flow in Pub Page

+208 -173
+1 -1
app/lish/[did]/[publication]/PublicationAuthor.tsx
··· 11 11 <ProfilePopover 12 12 didOrHandle={props.did} 13 13 trigger={ 14 - <span className="hover:underline"> 14 + <span className="hover:text-accent-contrast"> 15 15 <strong>by {props.displayName}</strong> @{props.handle} 16 16 </span> 17 17 }
+1 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 15 15 import { type CommentOnDocument } from "contexts/DocumentContext"; 16 16 import { prefetchQuotesData } from "./Quotes"; 17 17 import { useIdentityData } from "components/IdentityProvider"; 18 - import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; 18 + import { ManageSubscription } from "app/lish/Subscribe"; 19 19 import { EditTiny } from "components/Icons/EditTiny"; 20 20 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 21 21 import { RecommendButton } from "components/RecommendButton";
+5 -7
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 1 1 "use client"; 2 2 import { PubLeafletPagesLinearDocument } from "lexicons/api"; 3 3 import { useLeafletContent } from "contexts/LeafletContentContext"; 4 - import { PostPageData } from "./getPostPageData"; 5 - import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 6 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 7 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 8 - import { EditTiny } from "components/Icons/EditTiny"; 9 4 import { 10 5 ExpandedInteractions, 11 6 getCommentCount, ··· 74 69 pageOptions={pageOptions} 75 70 footnoteSideColumn={ 76 71 !props.hasContentToRight ? ( 77 - <PublishedFootnoteSideColumn footnotes={footnotes} fullPageScroll={fullPageScroll} /> 72 + <PublishedFootnoteSideColumn 73 + footnotes={footnotes} 74 + fullPageScroll={fullPageScroll} 75 + /> 78 76 ) : undefined 79 77 } 80 78 > ··· 96 94 footnoteIndexMap={footnoteIndexMap} 97 95 /> 98 96 <PublishedFootnoteSection footnotes={footnotes} /> 99 - <PostSubscribe /> 97 + {/*<PostSubscribe />*/} 100 98 <PostPrevNextButtons 101 99 showPrevNext={preferences.showPrevNext !== false && !isSubpage} 102 100 />
+13 -7
app/lish/[did]/[publication]/[rkey]/PostPubInfo.tsx
··· 1 1 import { SubscribeButton } from "components/Subscribe/SubscribeButton"; 2 2 3 - export const PostPubInfo = () => { 4 - let newsletterMode = true; 5 - let user = { 3 + export const dummy = { 4 + newsletterMode: true, 5 + user: { 6 6 loggedIn: false, 7 7 email: undefined, 8 8 handle: undefined, 9 - subscribed: false, 10 - }; 9 + subscribed: true, 10 + }, 11 + }; 12 + 13 + export const PostPubInfo = () => { 11 14 return ( 12 15 <div className="px-3 sm:px-4 w-full"> 13 16 <div className="accent-container rounded-lg! w-full px-3 pt-3 pb-4 sm:px-4 sm:pt-4 sm:pb-5 text-center justify-center"> 14 17 <h3 className="leading-snug text-secondary">Pub Title here</h3> 15 18 <div className="text-tertiary pb-3">this is the pubs description</div> 16 - {user.subscribed ? ( 19 + {dummy.user.subscribed ? ( 17 20 <div>Manage Sub</div> 18 21 ) : ( 19 - <SubscribeButton newsletterMode={newsletterMode} user={user} /> 22 + <SubscribeButton 23 + newsletterMode={dummy.newsletterMode} 24 + user={dummy.user} 25 + /> 20 26 )} 21 27 </div> 22 28 </div>
+135 -123
app/lish/[did]/[publication]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 2 import { AtUri } from "@atproto/syntax"; 3 - import { getPublicationURL, getDocumentURL } from "app/lish/createPub/getPublicationURL"; 3 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 4 4 import { BskyAgent } from "@atproto/api"; 5 5 import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; 6 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 7 6 import React from "react"; 8 7 import { 9 8 PublicationBackgroundProvider, ··· 15 14 import { LocalizedDate } from "./LocalizedDate"; 16 15 import { PublicationHomeLayout } from "./PublicationHomeLayout"; 17 16 import { PublicationAuthor } from "./PublicationAuthor"; 18 - import { Separator } from "components/Layout"; 19 17 import { 20 18 normalizePublicationRecord, 21 19 normalizeDocumentRecord, 22 20 } from "src/utils/normalizeRecords"; 23 21 import { getFirstParagraph } from "src/utils/getFirstParagraph"; 24 22 import { FontLoader } from "components/FontLoader"; 23 + import { SubscribeButton } from "components/Subscribe/SubscribeButton"; 24 + import { dummy } from "./[rkey]/PostPubInfo"; 25 25 26 26 export default async function Publication(props: { 27 27 params: Promise<{ publication: string; did: string }>; ··· 61 61 try { 62 62 return ( 63 63 <> 64 - <FontLoader headingFontId={record?.theme?.headingFont} bodyFontId={record?.theme?.bodyFont} /> 65 - <PublicationThemeProvider 66 - theme={record?.theme} 67 - pub_creator={publication.identity_did} 68 - > 69 - <PublicationBackgroundProvider 64 + <FontLoader 65 + headingFontId={record?.theme?.headingFont} 66 + bodyFontId={record?.theme?.bodyFont} 67 + /> 68 + <PublicationThemeProvider 70 69 theme={record?.theme} 71 70 pub_creator={publication.identity_did} 72 71 > 73 - <PublicationHomeLayout 74 - uri={publication.uri} 75 - showPageBackground={!!showPageBackground} 72 + <PublicationBackgroundProvider 73 + theme={record?.theme} 74 + pub_creator={publication.identity_did} 76 75 > 77 - <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 78 - {record?.icon && ( 79 - <div 80 - className="shrink-0 w-10 h-10 rounded-full mx-auto" 81 - style={{ 82 - backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 83 - backgroundRepeat: "no-repeat", 84 - backgroundPosition: "center", 85 - backgroundSize: "cover", 86 - }} 87 - /> 88 - )} 89 - <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 90 - {publication.name} 91 - </h2> 92 - <p className="sm:text-lg text-secondary"> 93 - {record?.description}{" "} 94 - </p> 95 - {profile && ( 96 - <PublicationAuthor 97 - did={profile.did} 98 - displayName={profile.displayName} 99 - handle={profile.handle} 100 - /> 101 - )} 102 - <div className="sm:pt-4 pt-4"> 76 + <PublicationHomeLayout 77 + uri={publication.uri} 78 + showPageBackground={!!showPageBackground} 79 + > 80 + <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 81 + {record?.icon && ( 82 + <div 83 + className="shrink-0 w-10 h-10 rounded-full mx-auto" 84 + style={{ 85 + backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 86 + backgroundRepeat: "no-repeat", 87 + backgroundPosition: "center", 88 + backgroundSize: "cover", 89 + }} 90 + /> 91 + )} 92 + <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 93 + {publication.name} 94 + </h2> 95 + <p className="sm:text-lg text-secondary"> 96 + {record?.description}{" "} 97 + </p> 98 + {profile && ( 99 + <PublicationAuthor 100 + did={profile.did} 101 + displayName={profile.displayName} 102 + handle={profile.handle} 103 + /> 104 + )} 105 + <div className="spacer h-4 w-full" /> 106 + <SubscribeButton {...dummy} /> 107 + {/*<div className="sm:pt-4 pt-4"> 103 108 <SubscribeWithBluesky 104 109 base_url={getPublicationURL(publication)} 105 110 pubName={publication.name} 106 111 pub_uri={publication.uri} 107 112 subscribers={publication.publication_subscriptions} 108 113 /> 114 + </div>*/} 109 115 </div> 110 - </div> 111 - <div className="publicationPostList w-full flex flex-col gap-4"> 112 - {publication.documents_in_publications 113 - .filter((d) => !!d?.documents) 114 - .sort((a, b) => { 115 - const aRecord = normalizeDocumentRecord(a.documents?.data); 116 - const bRecord = normalizeDocumentRecord(b.documents?.data); 117 - const aDate = aRecord?.publishedAt 118 - ? new Date(aRecord.publishedAt) 119 - : new Date(0); 120 - const bDate = bRecord?.publishedAt 121 - ? new Date(bRecord.publishedAt) 122 - : new Date(0); 123 - return bDate.getTime() - aDate.getTime(); // Sort by most recent first 124 - }) 125 - .map((doc) => { 126 - if (!doc.documents) return null; 127 - const doc_record = normalizeDocumentRecord( 128 - doc.documents.data, 129 - ); 130 - if (!doc_record) return null; 131 - let uri = new AtUri(doc.documents.uri); 132 - let quotes = 133 - doc.documents.document_mentions_in_bsky[0].count || 0; 134 - let comments = 135 - record?.preferences?.showComments === false 136 - ? 0 137 - : doc.documents.comments_on_documents[0].count || 0; 138 - let recommends = 139 - doc.documents.recommends_on_documents?.[0]?.count || 0; 140 - let tags = doc_record.tags || []; 116 + <div className="publicationPostList w-full flex flex-col gap-4"> 117 + {publication.documents_in_publications 118 + .filter((d) => !!d?.documents) 119 + .sort((a, b) => { 120 + const aRecord = normalizeDocumentRecord(a.documents?.data); 121 + const bRecord = normalizeDocumentRecord(b.documents?.data); 122 + const aDate = aRecord?.publishedAt 123 + ? new Date(aRecord.publishedAt) 124 + : new Date(0); 125 + const bDate = bRecord?.publishedAt 126 + ? new Date(bRecord.publishedAt) 127 + : new Date(0); 128 + return bDate.getTime() - aDate.getTime(); // Sort by most recent first 129 + }) 130 + .map((doc) => { 131 + if (!doc.documents) return null; 132 + const doc_record = normalizeDocumentRecord( 133 + doc.documents.data, 134 + ); 135 + if (!doc_record) return null; 136 + let uri = new AtUri(doc.documents.uri); 137 + let quotes = 138 + doc.documents.document_mentions_in_bsky[0].count || 0; 139 + let comments = 140 + record?.preferences?.showComments === false 141 + ? 0 142 + : doc.documents.comments_on_documents[0].count || 0; 143 + let recommends = 144 + doc.documents.recommends_on_documents?.[0]?.count || 0; 145 + let tags = doc_record.tags || []; 141 146 142 - const docUrl = getDocumentURL(doc_record, doc.documents.uri, publication); 143 - return ( 144 - <React.Fragment key={doc.documents?.uri}> 145 - <div className="flex w-full grow flex-col "> 146 - <SpeedyLink 147 - href={docUrl} 148 - className="publishedPost hover:no-underline! flex flex-col" 149 - > 150 - {doc_record.title && ( 151 - <h3 className="text-primary">{doc_record.title}</h3> 152 - )} 153 - <p className="italic text-secondary line-clamp-3"> 154 - {doc_record.description || getFirstParagraph(doc_record)} 155 - </p> 156 - </SpeedyLink> 147 + const docUrl = getDocumentURL( 148 + doc_record, 149 + doc.documents.uri, 150 + publication, 151 + ); 152 + return ( 153 + <React.Fragment key={doc.documents?.uri}> 154 + <div className="flex w-full grow flex-col "> 155 + <SpeedyLink 156 + href={docUrl} 157 + className="publishedPost hover:no-underline! flex flex-col" 158 + > 159 + {doc_record.title && ( 160 + <h3 className="text-primary"> 161 + {doc_record.title} 162 + </h3> 163 + )} 164 + <p className="italic text-secondary line-clamp-3"> 165 + {doc_record.description || 166 + getFirstParagraph(doc_record)} 167 + </p> 168 + </SpeedyLink> 157 169 158 - <div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 159 - <p className="text-sm text-tertiary "> 160 - {doc_record.publishedAt && ( 161 - <LocalizedDate 162 - dateString={doc_record.publishedAt} 163 - options={{ 164 - year: "numeric", 165 - month: "long", 166 - day: "2-digit", 167 - }} 168 - /> 169 - )}{" "} 170 - </p> 170 + <div className="justify-between w-full text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 171 + <p className="text-sm text-tertiary "> 172 + {doc_record.publishedAt && ( 173 + <LocalizedDate 174 + dateString={doc_record.publishedAt} 175 + options={{ 176 + year: "numeric", 177 + month: "long", 178 + day: "2-digit", 179 + }} 180 + /> 181 + )}{" "} 182 + </p> 171 183 172 - <InteractionPreview 173 - quotesCount={quotes} 174 - commentsCount={comments} 175 - recommendsCount={recommends} 176 - documentUri={doc.documents.uri} 177 - tags={tags} 178 - postUrl={docUrl} 179 - showComments={ 180 - record?.preferences?.showComments !== false 181 - } 182 - showMentions={ 183 - record?.preferences?.showMentions !== false 184 - } 185 - showRecommends={ 186 - record?.preferences?.showRecommends !== false 187 - } 188 - /> 184 + <InteractionPreview 185 + quotesCount={quotes} 186 + commentsCount={comments} 187 + recommendsCount={recommends} 188 + documentUri={doc.documents.uri} 189 + tags={tags} 190 + postUrl={docUrl} 191 + showComments={ 192 + record?.preferences?.showComments !== false 193 + } 194 + showMentions={ 195 + record?.preferences?.showMentions !== false 196 + } 197 + showRecommends={ 198 + record?.preferences?.showRecommends !== false 199 + } 200 + /> 201 + </div> 189 202 </div> 190 - </div> 191 - <hr className="last:hidden border-border-light" /> 192 - </React.Fragment> 193 - ); 194 - })} 195 - </div> 196 - </PublicationHomeLayout> 197 - </PublicationBackgroundProvider> 198 - </PublicationThemeProvider> 203 + <hr className="last:hidden border-border-light" /> 204 + </React.Fragment> 205 + ); 206 + })} 207 + </div> 208 + </PublicationHomeLayout> 209 + </PublicationBackgroundProvider> 210 + </PublicationThemeProvider> 199 211 </> 200 212 ); 201 213 } catch (e) {
+48 -30
components/Subscribe/AtSubscribe.tsx components/Subscribe/HandleSubscribe.tsx
··· 1 - import { ManageSubscription } from "app/lish/Subscribe"; 1 + "use client"; 2 2 import { ButtonPrimary } from "components/Buttons"; 3 3 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 4 - import { GoToArrow } from "components/Icons/GoToArrow"; 5 4 import { Input } from "components/Input"; 6 - import { Modal } from "components/Modal"; 7 5 import { Popover } from "components/Popover"; 8 6 import Link from "next/link"; 9 7 import { useState } from "react"; ··· 14 12 LogoLeaflet, 15 13 LogoTangled, 16 14 } from "./Logos"; 15 + import { GoToArrow } from "components/Icons/GoToArrow"; 16 + import { Separator } from "components/Layout"; 17 17 18 - export const AtSubscribe = (props: { 18 + export const HandleSubscribe = (props: { 19 19 compact?: boolean; 20 20 user: { 21 21 loggedIn: boolean; ··· 23 23 handle: string | undefined; 24 24 }; 25 25 }) => { 26 - if (props.user.loggedIn) { 26 + if (props.user.loggedIn && props.user.handle) { 27 27 return ( 28 28 <ButtonPrimary className="mx-auto max-w-full"> 29 29 <span className="shrink-0">Subscribe as</span> ··· 38 38 return ( 39 39 <div className="max-w-sm mx-auto"> 40 40 <HandleInput compact={props.compact} /> 41 - <div className="flex justify-between pt-0.5"> 42 - <UniversalHandleInfo />{" "} 41 + <div className="flex gap-2 justify-center items-center mx-auto pt-0.5 "> 42 + <UniversalHandleInfo /> 43 + <Separator classname="h-3! border-accent-contrast!" /> 43 44 <div className="text-sm text-accent-contrast font-bold">Create</div> 44 45 </div> 45 46 </div> ··· 49 50 export const HandleInput = (props: { compact?: boolean }) => { 50 51 let [handleValue, setHandleValue] = useState(""); 51 52 return ( 52 - <div className="flex flex-col"> 53 - <div className="input-with-border pl-0! py-0! flex gap-0 "> 54 - <div className="border-r border-border text-center w-7 mr-2">@</div> 55 - <Input 56 - className="appearance-none! outline-none! py-0.5 grow max-w-full" 57 - placeholder="universal.handle" 58 - size={30} 59 - value={handleValue} 60 - onChange={(e) => setHandleValue(e.target.value)} 61 - /> 53 + <div className="handleInput input-with-border relative pl-0! py-0! flex gap-0 "> 54 + <div className="border-r border-border text-center w-7 shrink-0 mr-2"> 55 + @ 62 56 </div> 63 - {!props.compact && ( 64 - <> 65 - <div className="w-full flex gap-2 items-center mt-2 mb-3 "> 66 - <hr className="grow border-border-light" /> 67 - <div className="shrink-0 text-sm italix text-tertiary"> 68 - or link with 69 - </div> 70 - <hr className="grow border-border-light" /> 71 - </div> 72 - <ButtonPrimary fullWidth> 73 - <BlueskyTiny /> Bluesky 74 - </ButtonPrimary> 75 - </> 57 + <Input 58 + className={`appearance-none! outline-none! py-0.5 ${props.compact ? "pr-6" : "pr-14"} grow max-w-full`} 59 + placeholder="universal.handle" 60 + size={30} 61 + value={handleValue} 62 + onChange={(e) => setHandleValue(e.target.value)} 63 + /> 64 + 65 + {props.compact ? ( 66 + <button className="absolute text-sm py-0! right-[6px] top-[6px] leading-snug outline-none!"> 67 + <GoToArrow /> 68 + </button> 69 + ) : ( 70 + <ButtonPrimary 71 + compact 72 + className="absolute text-sm py-0! right-[3px] top-[3.5px] leading-snug outline-none!" 73 + > 74 + Subscribe 75 + </ButtonPrimary> 76 76 )} 77 + </div> 78 + ); 79 + }; 80 + 81 + export const HandleInputandOAuth = () => { 82 + return ( 83 + <div className="handleInputAndOAuth flex flex-col"> 84 + <HandleInput /> 85 + <div className="w-full flex gap-2 items-center mt-2 mb-3 "> 86 + <hr className="grow border-border-light" /> 87 + <div className="shrink-0 text-sm italix text-tertiary"> 88 + or link with 89 + </div> 90 + <hr className="grow border-border-light" /> 91 + </div> 92 + <ButtonPrimary fullWidth> 93 + <BlueskyTiny /> Bluesky 94 + </ButtonPrimary> 77 95 </div> 78 96 ); 79 97 };
+3 -2
components/Subscribe/EmailSubscribe.tsx
··· 1 + "use client"; 1 2 import * as OneTimePasswordField from "@radix-ui/react-one-time-password-field"; 2 3 import { ButtonPrimary } from "components/Buttons"; 3 4 import { GoToArrow } from "components/Icons/GoToArrow"; 4 5 import { Input } from "components/Input"; 5 6 import { Modal } from "components/Modal"; 6 7 import { useState } from "react"; 7 - import { HandleInput, UniversalHandleInfo } from "./AtSubscribe"; 8 + import { HandleInputandOAuth, UniversalHandleInfo } from "./HandleSubscribe"; 8 9 9 10 export const EmailSubscribe = (props: { 10 11 compact?: boolean; ··· 117 118 </div> 118 119 <UniversalHandleInfo /> 119 120 </div> 120 - <HandleInput /> 121 + <HandleInputandOAuth /> 121 122 </div> 122 123 </> 123 124 )}
+2 -2
components/Subscribe/SubscribeButton.tsx
··· 1 - import { AtSubscribe } from "./AtSubscribe"; 1 + import { HandleSubscribe } from "./HandleSubscribe"; 2 2 import { EmailSubscribe } from "./EmailSubscribe"; 3 3 4 4 export const SubscribeButton = (props: { ··· 11 11 }) => { 12 12 if (props.newsletterMode) { 13 13 return <EmailSubscribe user={props.user} />; 14 - } else return <AtSubscribe user={props.user} compact />; 14 + } else return <HandleSubscribe user={props.user} />; 15 15 };