a tool for shared writing and social publishing
0

Configure Feed

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

add signup blocks

Jared Pereira (May 28, 2026, 5:02 PM EDT) ea34eecd 39bafdf2

+186 -11
+1 -1
actions/createNewLeaflet.ts
··· 8 8 import { cookies } from "next/headers"; 9 9 import { pool } from "supabase/pool"; 10 10 11 - type DefaultBlockType = "h1" | "text" | "posts-list"; 11 + export type DefaultBlockType = "h1" | "text" | "posts-list" | "signup"; 12 12 13 13 type FactRow = { 14 14 id: string;
+8 -2
actions/createPublicationPage.ts
··· 1 1 "use server"; 2 2 import { generateKeyBetween } from "fractional-indexing"; 3 3 import { getIdentityData } from "actions/getIdentityData"; 4 - import { createNewLeaflet } from "./createNewLeaflet"; 4 + import { createNewLeaflet, type DefaultBlockType } from "./createNewLeaflet"; 5 5 import { supabaseServerClient } from "supabase/serverClient"; 6 6 7 7 export async function createPublicationPage(args: { ··· 10 10 title?: string; 11 11 sort_order?: string; 12 12 includePostsList?: boolean; 13 + includeSignup?: boolean; 13 14 }) { 14 15 let identity = await getIdentityData(); 15 16 if (!identity || !identity.atp_did) return null; ··· 34 35 sort_order = generateKeyBetween(last?.sort_order ?? null, null); 35 36 } 36 37 38 + let firstBlocks: DefaultBlockType[] = [ 39 + "text", 40 + ...(args.includeSignup ? (["signup"] as const) : []), 41 + ...(args.includePostsList ? (["posts-list"] as const) : []), 42 + ]; 37 43 let leaflet_src = await createNewLeaflet({ 38 44 pageType: "doc", 39 45 redirectUser: false, 40 - firstBlocks: args.includePostsList ? ["text", "posts-list"] : ["text"], 46 + firstBlocks, 41 47 addToHomepage: false, 42 48 }); 43 49
+20 -2
app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 19 19 PubLeafletBlocksPoll, 20 20 PubLeafletBlocksPostsList, 21 21 PubLeafletBlocksButton, 22 + PubLeafletBlocksSignup, 22 23 } from "lexicons/api"; 23 24 import { 24 25 PublicationPostsList, ··· 44 45 import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; 45 46 import { PollData } from "./fetchPollData"; 46 47 import { ButtonPrimary } from "components/Buttons"; 48 + import { SubscribePanel } from "components/Subscribe/SubscribeButton"; 47 49 import { blockTextSize } from "src/utils/blockTextSize"; 48 50 import { slugify } from "src/utils/slugify"; 49 51 import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; ··· 157 159 isLast?: boolean; 158 160 }) => { 159 161 let b = block; 160 - let currentPublicationUri = 161 - useDocumentOptional()?.publication?.uri ?? null; 162 + let document = useDocumentOptional(); 163 + let currentPublicationUri = document?.publication?.uri ?? null; 162 164 let blockProps = { 163 165 style: { 164 166 scrollMarginTop: "4rem", ··· 279 281 } 280 282 case PubLeafletBlocksHorizontalRule.isMain(b.block): { 281 283 return <hr className="my-2 w-full border-border-light" />; 284 + } 285 + case PubLeafletBlocksSignup.isMain(b.block): { 286 + if (!document?.publication?.uri) return null; 287 + return ( 288 + <div className={className} {...blockProps}> 289 + <SubscribePanel 290 + publicationUri={document.publication.uri} 291 + publicationUrl={document.normalizedPublication?.url} 292 + publicationName={ 293 + document.normalizedPublication?.name ?? document.publication.name 294 + } 295 + publicationDescription={document.normalizedPublication?.description} 296 + newsletterMode={document.publication.newsletterMode} 297 + /> 298 + </div> 299 + ); 282 300 } 283 301 case PubLeafletBlocksPostsList.isMain(b.block): { 284 302 if (!postsListData) return null;
+2 -1
app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
··· 37 37 name: string; 38 38 identity_did: string; 39 39 record: unknown; 40 + publication_newsletter_settings?: { enabled: boolean } | null; 40 41 publication_pages?: { 41 42 id: number; 42 43 path: string | null; ··· 145 146 DocumentContextValue["publication"] 146 147 >["record"], 147 148 publication_subscriptions: [], 148 - newsletterMode: false, 149 + newsletterMode: !!publication.publication_newsletter_settings?.enabled, 149 150 pages: (publication.publication_pages ?? []).filter((p) => p.record_uri), 150 151 }, 151 152 commentsCount: 0,
+1
app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx
··· 32 32 path: "/", 33 33 title: "home", 34 34 includePostsList: true, 35 + includeSignup: true, 35 36 }); 36 37 setLoading(false); 37 38 if (!created) return;
+2
components/Blocks/Block.tsx
··· 32 32 import { CodeBlock } from "./CodeBlock"; 33 33 import { HorizontalRule } from "./HorizontalRule"; 34 34 import { PostsListBlock } from "./PostsListBlock"; 35 + import { SignupBlock } from "./SignupBlock"; 35 36 import { deepEquals } from "src/utils/deepEquals"; 36 37 import { isTextBlock } from "src/utils/isTextBlock"; 37 38 import { DeleteTiny } from "components/Icons/DeleteTiny"; ··· 363 364 "standard-site-post": StandardSitePostBlock, 364 365 "horizontal-rule": HorizontalRule, 365 366 "posts-list": PostsListBlock, 367 + signup: SignupBlock, 366 368 }; 367 369 368 370 export const BlockMultiselectIndicator = (props: BlockProps) => {
+2 -2
components/Blocks/BlockCommands.tsx
··· 458 458 name: "Subscribe Form", 459 459 icon: <BlockMailboxSmall />, 460 460 type: "publication", 461 - alternateNames: ["subscribe", "newsletter", "email"], 461 + alternateNames: ["subscribe", "newsletter", "email", "signup"], 462 462 publicationOnly: true, 463 463 onSelect: async (rep, props) => { 464 464 props.entityID && clearCommandSearchText(props.entityID); 465 - await createBlockWithType(rep, props, "text"); 465 + await createBlockWithType(rep, props, "signup"); 466 466 }, 467 467 }, 468 468 ];
+57
components/Blocks/SignupBlock.tsx
··· 1 + import { useState } from "react"; 2 + import { useUIState } from "src/useUIState"; 3 + import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 4 + import { BlockProps, BlockLayout } from "./Block"; 5 + import { EmailInput } from "components/Subscribe/EmailSubscribe"; 6 + import { ButtonPrimary } from "components/Buttons"; 7 + 8 + // SignupBlock is a static, non-interactive preview of the publication subscribe 9 + // form, styled to match SubscribePanel (components/Subscribe/SubscribeButton.tsx). 10 + // It is shown in the editor — the real, working form is rendered on the 11 + // published page (see PostContent.tsx's signup case). No user data is fetched. 12 + export const SignupBlock = ( 13 + props: BlockProps & { 14 + areYouSure?: boolean; 15 + setAreYouSure?: (value: boolean) => void; 16 + }, 17 + ) => { 18 + let isSelected = useUIState((s) => 19 + s.selectedBlocks.find((b) => b.value === props.entityID), 20 + ); 21 + let { normalizedPublication } = useLeafletPublicationData(); 22 + let [email, setEmail] = useState(""); 23 + 24 + let publicationName = normalizedPublication?.name || "Subscribe"; 25 + let publicationDescription = normalizedPublication?.description; 26 + 27 + return ( 28 + <BlockLayout 29 + isSelected={!!isSelected} 30 + areYouSure={props.areYouSure} 31 + setAreYouSure={props.setAreYouSure} 32 + className="accent-container rounded-lg! border-none! p-0! text-center justify-center" 33 + > 34 + <div className="px-3 pt-3 pb-4 sm:px-4 sm:pt-4 sm:pb-5"> 35 + <h3 className="leading-snug text-secondary">{publicationName}</h3> 36 + {publicationDescription && ( 37 + <div className="text-tertiary pb-1">{publicationDescription}</div> 38 + )} 39 + <div className="max-w-sm w-full mx-auto pt-2"> 40 + <EmailInput 41 + value={email} 42 + onChange={setEmail} 43 + action={ 44 + <ButtonPrimary 45 + type="button" 46 + compact 47 + className="leading-tight! outline-none! text-sm!" 48 + > 49 + Subscribe 50 + </ButtonPrimary> 51 + } 52 + /> 53 + </div> 54 + </div> 55 + </BlockLayout> 56 + ); 57 + };
+2
lexicons/api/index.ts
··· 40 40 import * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' 41 41 import * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' 42 42 import * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList' 43 + import * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup' 43 44 import * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost' 44 45 import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 45 46 import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' ··· 96 97 export * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' 97 98 export * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' 98 99 export * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList' 100 + export * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup' 99 101 export * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost' 100 102 export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 101 103 export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList'
+16
lexicons/api/lexicons.ts
··· 1590 1590 }, 1591 1591 }, 1592 1592 }, 1593 + PubLeafletBlocksSignup: { 1594 + lexicon: 1, 1595 + id: 'pub.leaflet.blocks.signup', 1596 + defs: { 1597 + main: { 1598 + type: 'object', 1599 + description: 1600 + "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", 1601 + required: [], 1602 + properties: {}, 1603 + }, 1604 + }, 1605 + }, 1593 1606 PubLeafletBlocksStandardSitePost: { 1594 1607 lexicon: 1, 1595 1608 id: 'pub.leaflet.blocks.standardSitePost', ··· 2001 2014 'lex:pub.leaflet.blocks.poll', 2002 2015 'lex:pub.leaflet.blocks.button', 2003 2016 'lex:pub.leaflet.blocks.postsList', 2017 + 'lex:pub.leaflet.blocks.signup', 2004 2018 ], 2005 2019 }, 2006 2020 x: { ··· 2105 2119 'lex:pub.leaflet.blocks.poll', 2106 2120 'lex:pub.leaflet.blocks.button', 2107 2121 'lex:pub.leaflet.blocks.postsList', 2122 + 'lex:pub.leaflet.blocks.signup', 2108 2123 ], 2109 2124 }, 2110 2125 alignment: { ··· 2981 2996 PubLeafletBlocksPage: 'pub.leaflet.blocks.page', 2982 2997 PubLeafletBlocksPoll: 'pub.leaflet.blocks.poll', 2983 2998 PubLeafletBlocksPostsList: 'pub.leaflet.blocks.postsList', 2999 + PubLeafletBlocksSignup: 'pub.leaflet.blocks.signup', 2984 3000 PubLeafletBlocksStandardSitePost: 'pub.leaflet.blocks.standardSitePost', 2985 3001 PubLeafletBlocksText: 'pub.leaflet.blocks.text', 2986 3002 PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList',
+30
lexicons/api/types/pub/leaflet/blocks/signup.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'pub.leaflet.blocks.signup' 16 + 17 + /** A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data. */ 18 + export interface Main { 19 + $type?: 'pub.leaflet.blocks.signup' 20 + } 21 + 22 + const hashMain = 'main' 23 + 24 + export function isMain<V>(v: V) { 25 + return is$typed(v, id, hashMain) 26 + } 27 + 28 + export function validateMain<V>(v: V) { 29 + return validate<Main & V>(v, id, hashMain) 30 + }
+2
lexicons/api/types/pub/leaflet/pages/canvas.ts
··· 26 26 import type * as PubLeafletBlocksPoll from '../blocks/poll' 27 27 import type * as PubLeafletBlocksButton from '../blocks/button' 28 28 import type * as PubLeafletBlocksPostsList from '../blocks/postsList' 29 + import type * as PubLeafletBlocksSignup from '../blocks/signup' 29 30 30 31 const is$typed = _is$typed, 31 32 validate = _validate ··· 67 68 | $Typed<PubLeafletBlocksPoll.Main> 68 69 | $Typed<PubLeafletBlocksButton.Main> 69 70 | $Typed<PubLeafletBlocksPostsList.Main> 71 + | $Typed<PubLeafletBlocksSignup.Main> 70 72 | { $type: string } 71 73 x: number 72 74 y: number
+2
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 26 26 import type * as PubLeafletBlocksPoll from '../blocks/poll' 27 27 import type * as PubLeafletBlocksButton from '../blocks/button' 28 28 import type * as PubLeafletBlocksPostsList from '../blocks/postsList' 29 + import type * as PubLeafletBlocksSignup from '../blocks/signup' 29 30 30 31 const is$typed = _is$typed, 31 32 validate = _validate ··· 67 68 | $Typed<PubLeafletBlocksPoll.Main> 68 69 | $Typed<PubLeafletBlocksButton.Main> 69 70 | $Typed<PubLeafletBlocksPostsList.Main> 71 + | $Typed<PubLeafletBlocksSignup.Main> 70 72 | { $type: string } 71 73 alignment?: 72 74 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
+12
lexicons/pub/leaflet/blocks/signup.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.signup", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", 8 + "required": [], 9 + "properties": {} 10 + } 11 + } 12 + }
+2 -1
lexicons/pub/leaflet/pages/canvas.json
··· 48 48 "pub.leaflet.blocks.page", 49 49 "pub.leaflet.blocks.poll", 50 50 "pub.leaflet.blocks.button", 51 - "pub.leaflet.blocks.postsList" 51 + "pub.leaflet.blocks.postsList", 52 + "pub.leaflet.blocks.signup" 52 53 ] 53 54 }, 54 55 "x": {
+2 -1
lexicons/pub/leaflet/pages/linearDocument.json
··· 45 45 "pub.leaflet.blocks.page", 46 46 "pub.leaflet.blocks.poll", 47 47 "pub.leaflet.blocks.button", 48 - "pub.leaflet.blocks.postsList" 48 + "pub.leaflet.blocks.postsList", 49 + "pub.leaflet.blocks.signup" 49 50 ] 50 51 }, 51 52 "alignment": {
+15
lexicons/src/blocks.ts
··· 382 382 }, 383 383 }; 384 384 385 + export const PubLeafletBlocksSignup: LexiconDoc = { 386 + lexicon: 1, 387 + id: "pub.leaflet.blocks.signup", 388 + defs: { 389 + main: { 390 + type: "object", 391 + description: 392 + "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", 393 + required: [], 394 + properties: {}, 395 + }, 396 + }, 397 + }; 398 + 385 399 export const BlockLexicons = [ 386 400 PubLeafletBlocksIFrame, 387 401 PubLeafletBlocksText, ··· 400 414 PubLeafletBlocksPoll, 401 415 PubLeafletBlocksButton, 402 416 PubLeafletBlocksPostsList, 417 + PubLeafletBlocksSignup, 403 418 ]; 404 419 export const BlockUnion: LexRefUnion = { 405 420 type: "union",
+2 -1
src/replicache/attributes.ts
··· 403 403 | "code" 404 404 | "blockquote" 405 405 | "horizontal-rule" 406 - | "posts-list"; 406 + | "posts-list" 407 + | "signup"; 407 408 }; 408 409 "canvas-pattern-union": { 409 410 type: "canvas-pattern-union";
+7
src/utils/factsToPagesRecord.ts
··· 18 18 PubLeafletBlocksPage, 19 19 PubLeafletBlocksPoll, 20 20 PubLeafletBlocksPostsList, 21 + PubLeafletBlocksSignup, 21 22 PubLeafletBlocksText, 22 23 PubLeafletBlocksUnorderedList, 23 24 PubLeafletBlocksWebsite, ··· 566 567 ...(viewFact && { view: viewFact.data.value }), 567 568 ...(highlightFact && { highlightFirstPost: highlightFact.data.value }), 568 569 ...(filterTagFact && { filterByTag: filterTagFact.data.value }), 570 + }; 571 + return block; 572 + } 573 + if (b.type === "signup") { 574 + const block: $Typed<PubLeafletBlocksSignup.Main> = { 575 + $type: ids.PubLeafletBlocksSignup, 569 576 }; 570 577 return block; 571 578 }
+1
src/utils/getBlocksAsHTML.tsx
··· 88 88 mailbox: async () => null, 89 89 poll: async () => null, 90 90 embed: async () => null, 91 + signup: async () => null, 91 92 "bluesky-post": async (b, tx) => { 92 93 let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); 93 94 if (!post) return null;