diff --git a/actions/createNewLeaflet.ts b/actions/createNewLeaflet.ts index 77d3a594..26fe608e 100644 --- a/actions/createNewLeaflet.ts +++ b/actions/createNewLeaflet.ts @@ -8,7 +8,7 @@ import { generateKeyBetween } from "fractional-indexing"; import { cookies } from "next/headers"; import { pool } from "supabase/pool"; -type DefaultBlockType = "h1" | "text" | "posts-list"; +export type DefaultBlockType = "h1" | "text" | "posts-list" | "signup"; type FactRow = { id: string; diff --git a/actions/createPublicationPage.ts b/actions/createPublicationPage.ts index 88cac009..196d690d 100644 --- a/actions/createPublicationPage.ts +++ b/actions/createPublicationPage.ts @@ -1,7 +1,7 @@ "use server"; import { generateKeyBetween } from "fractional-indexing"; import { getIdentityData } from "actions/getIdentityData"; -import { createNewLeaflet } from "./createNewLeaflet"; +import { createNewLeaflet, type DefaultBlockType } from "./createNewLeaflet"; import { supabaseServerClient } from "supabase/serverClient"; export async function createPublicationPage(args: { @@ -10,6 +10,7 @@ export async function createPublicationPage(args: { title?: string; sort_order?: string; includePostsList?: boolean; + includeSignup?: boolean; }) { let identity = await getIdentityData(); if (!identity || !identity.atp_did) return null; @@ -34,10 +35,15 @@ export async function createPublicationPage(args: { sort_order = generateKeyBetween(last?.sort_order ?? null, null); } + let firstBlocks: DefaultBlockType[] = [ + "text", + ...(args.includeSignup ? (["signup"] as const) : []), + ...(args.includePostsList ? (["posts-list"] as const) : []), + ]; let leaflet_src = await createNewLeaflet({ pageType: "doc", redirectUser: false, - firstBlocks: args.includePostsList ? ["text", "posts-list"] : ["text"], + firstBlocks, addToHomepage: false, }); diff --git a/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx index 1091ad44..a55bc39c 100644 --- a/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -19,6 +19,7 @@ import { PubLeafletBlocksPoll, PubLeafletBlocksPostsList, PubLeafletBlocksButton, + PubLeafletBlocksSignup, } from "lexicons/api"; import { PublicationPostsList, @@ -44,6 +45,7 @@ import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock"; import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; import { PollData } from "./fetchPollData"; import { ButtonPrimary } from "components/Buttons"; +import { SubscribePanel } from "components/Subscribe/SubscribeButton"; import { blockTextSize } from "src/utils/blockTextSize"; import { slugify } from "src/utils/slugify"; import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; @@ -157,8 +159,8 @@ export let Block = ({ isLast?: boolean; }) => { let b = block; - let currentPublicationUri = - useDocumentOptional()?.publication?.uri ?? null; + let document = useDocumentOptional(); + let currentPublicationUri = document?.publication?.uri ?? null; let blockProps = { style: { scrollMarginTop: "4rem", @@ -280,6 +282,22 @@ export let Block = ({ case PubLeafletBlocksHorizontalRule.isMain(b.block): { return
; } + case PubLeafletBlocksSignup.isMain(b.block): { + if (!document?.publication?.uri) return null; + return ( +
+ +
+ ); + } case PubLeafletBlocksPostsList.isMain(b.block): { if (!postsListData) return null; const view: "compact" | "full" = diff --git a/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx b/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx index d97c4ae2..d4d77b0e 100644 --- a/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx +++ b/app/(app)/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx @@ -37,6 +37,7 @@ type PublicationRow = { name: string; identity_did: string; record: unknown; + publication_newsletter_settings?: { enabled: boolean } | null; publication_pages?: { id: number; path: string | null; @@ -145,7 +146,7 @@ export async function PublicationPageRenderer({ DocumentContextValue["publication"] >["record"], publication_subscriptions: [], - newsletterMode: false, + newsletterMode: !!publication.publication_newsletter_settings?.enabled, pages: (publication.publication_pages ?? []).filter((p) => p.record_uri), }, commentsCount: 0, diff --git a/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx b/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx index 2d087648..8df46369 100644 --- a/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx +++ b/app/(app)/lish/[did]/[publication]/dashboard/EditPagesNavLink.tsx @@ -32,6 +32,7 @@ export function EditPagesNavLink(props: { path: "/", title: "home", includePostsList: true, + includeSignup: true, }); setLoading(false); if (!created) return; diff --git a/components/Blocks/Block.tsx b/components/Blocks/Block.tsx index 4244ec7b..886fdf43 100644 --- a/components/Blocks/Block.tsx +++ b/components/Blocks/Block.tsx @@ -32,6 +32,7 @@ import { MathBlock } from "./MathBlock"; import { CodeBlock } from "./CodeBlock"; import { HorizontalRule } from "./HorizontalRule"; import { PostsListBlock } from "./PostsListBlock"; +import { SignupBlock } from "./SignupBlock"; import { deepEquals } from "src/utils/deepEquals"; import { isTextBlock } from "src/utils/isTextBlock"; import { DeleteTiny } from "components/Icons/DeleteTiny"; @@ -363,6 +364,7 @@ const BlockTypeComponents: { "standard-site-post": StandardSitePostBlock, "horizontal-rule": HorizontalRule, "posts-list": PostsListBlock, + signup: SignupBlock, }; export const BlockMultiselectIndicator = (props: BlockProps) => { diff --git a/components/Blocks/BlockCommands.tsx b/components/Blocks/BlockCommands.tsx index 0a7dcc82..815e0135 100644 --- a/components/Blocks/BlockCommands.tsx +++ b/components/Blocks/BlockCommands.tsx @@ -458,11 +458,11 @@ export const blockCommands: Command[] = [ name: "Subscribe Form", icon: , type: "publication", - alternateNames: ["subscribe", "newsletter", "email"], + alternateNames: ["subscribe", "newsletter", "email", "signup"], publicationOnly: true, onSelect: async (rep, props) => { props.entityID && clearCommandSearchText(props.entityID); - await createBlockWithType(rep, props, "text"); + await createBlockWithType(rep, props, "signup"); }, }, ]; diff --git a/components/Blocks/SignupBlock.tsx b/components/Blocks/SignupBlock.tsx new file mode 100644 index 00000000..404fde71 --- /dev/null +++ b/components/Blocks/SignupBlock.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { useUIState } from "src/useUIState"; +import { useLeafletPublicationData } from "components/PageSWRDataProvider"; +import { BlockProps, BlockLayout } from "./Block"; +import { EmailInput } from "components/Subscribe/EmailSubscribe"; +import { ButtonPrimary } from "components/Buttons"; + +// SignupBlock is a static, non-interactive preview of the publication subscribe +// form, styled to match SubscribePanel (components/Subscribe/SubscribeButton.tsx). +// It is shown in the editor — the real, working form is rendered on the +// published page (see PostContent.tsx's signup case). No user data is fetched. +export const SignupBlock = ( + props: BlockProps & { + areYouSure?: boolean; + setAreYouSure?: (value: boolean) => void; + }, +) => { + let isSelected = useUIState((s) => + s.selectedBlocks.find((b) => b.value === props.entityID), + ); + let { normalizedPublication } = useLeafletPublicationData(); + let [email, setEmail] = useState(""); + + let publicationName = normalizedPublication?.name || "Subscribe"; + let publicationDescription = normalizedPublication?.description; + + return ( + +
+

{publicationName}

+ {publicationDescription && ( +
{publicationDescription}
+ )} +
+ + Subscribe + + } + /> +
+
+
+ ); +}; diff --git a/lexicons/api/index.ts b/lexicons/api/index.ts index df865afe..66d13289 100644 --- a/lexicons/api/index.ts +++ b/lexicons/api/index.ts @@ -40,6 +40,7 @@ import * as PubLeafletBlocksOrderedList from './types/pub/leaflet/blocks/ordered import * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' import * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' import * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList' +import * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup' import * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost' import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' import * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' @@ -96,6 +97,7 @@ export * as PubLeafletBlocksOrderedList from './types/pub/leaflet/blocks/ordered export * as PubLeafletBlocksPage from './types/pub/leaflet/blocks/page' export * as PubLeafletBlocksPoll from './types/pub/leaflet/blocks/poll' export * as PubLeafletBlocksPostsList from './types/pub/leaflet/blocks/postsList' +export * as PubLeafletBlocksSignup from './types/pub/leaflet/blocks/signup' export * as PubLeafletBlocksStandardSitePost from './types/pub/leaflet/blocks/standardSitePost' export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' export * as PubLeafletBlocksUnorderedList from './types/pub/leaflet/blocks/unorderedList' diff --git a/lexicons/api/lexicons.ts b/lexicons/api/lexicons.ts index 6cb98832..10b7ce49 100644 --- a/lexicons/api/lexicons.ts +++ b/lexicons/api/lexicons.ts @@ -1590,6 +1590,19 @@ export const schemaDict = { }, }, }, + PubLeafletBlocksSignup: { + lexicon: 1, + id: 'pub.leaflet.blocks.signup', + defs: { + main: { + type: 'object', + description: + "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", + required: [], + properties: {}, + }, + }, + }, PubLeafletBlocksStandardSitePost: { lexicon: 1, id: 'pub.leaflet.blocks.standardSitePost', @@ -2001,6 +2014,7 @@ export const schemaDict = { 'lex:pub.leaflet.blocks.poll', 'lex:pub.leaflet.blocks.button', 'lex:pub.leaflet.blocks.postsList', + 'lex:pub.leaflet.blocks.signup', ], }, x: { @@ -2105,6 +2119,7 @@ export const schemaDict = { 'lex:pub.leaflet.blocks.poll', 'lex:pub.leaflet.blocks.button', 'lex:pub.leaflet.blocks.postsList', + 'lex:pub.leaflet.blocks.signup', ], }, alignment: { @@ -2981,6 +2996,7 @@ export const ids = { PubLeafletBlocksPage: 'pub.leaflet.blocks.page', PubLeafletBlocksPoll: 'pub.leaflet.blocks.poll', PubLeafletBlocksPostsList: 'pub.leaflet.blocks.postsList', + PubLeafletBlocksSignup: 'pub.leaflet.blocks.signup', PubLeafletBlocksStandardSitePost: 'pub.leaflet.blocks.standardSitePost', PubLeafletBlocksText: 'pub.leaflet.blocks.text', PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList', diff --git a/lexicons/api/types/pub/leaflet/blocks/signup.ts b/lexicons/api/types/pub/leaflet/blocks/signup.ts new file mode 100644 index 00000000..ea3a4ee0 --- /dev/null +++ b/lexicons/api/types/pub/leaflet/blocks/signup.ts @@ -0,0 +1,30 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'pub.leaflet.blocks.signup' + +/** A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data. */ +export interface Main { + $type?: 'pub.leaflet.blocks.signup' +} + +const hashMain = 'main' + +export function isMain(v: V) { + return is$typed(v, id, hashMain) +} + +export function validateMain(v: V) { + return validate
(v, id, hashMain) +} diff --git a/lexicons/api/types/pub/leaflet/pages/canvas.ts b/lexicons/api/types/pub/leaflet/pages/canvas.ts index e18bc2ed..8e0326fb 100644 --- a/lexicons/api/types/pub/leaflet/pages/canvas.ts +++ b/lexicons/api/types/pub/leaflet/pages/canvas.ts @@ -26,6 +26,7 @@ import type * as PubLeafletBlocksPage from '../blocks/page' import type * as PubLeafletBlocksPoll from '../blocks/poll' import type * as PubLeafletBlocksButton from '../blocks/button' import type * as PubLeafletBlocksPostsList from '../blocks/postsList' +import type * as PubLeafletBlocksSignup from '../blocks/signup' const is$typed = _is$typed, validate = _validate @@ -67,6 +68,7 @@ export interface Block { | $Typed | $Typed | $Typed + | $Typed | { $type: string } x: number y: number diff --git a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts index b35d8f98..fb2e36c6 100644 --- a/lexicons/api/types/pub/leaflet/pages/linearDocument.ts +++ b/lexicons/api/types/pub/leaflet/pages/linearDocument.ts @@ -26,6 +26,7 @@ import type * as PubLeafletBlocksPage from '../blocks/page' import type * as PubLeafletBlocksPoll from '../blocks/poll' import type * as PubLeafletBlocksButton from '../blocks/button' import type * as PubLeafletBlocksPostsList from '../blocks/postsList' +import type * as PubLeafletBlocksSignup from '../blocks/signup' const is$typed = _is$typed, validate = _validate @@ -67,6 +68,7 @@ export interface Block { | $Typed | $Typed | $Typed + | $Typed | { $type: string } alignment?: | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft' diff --git a/lexicons/pub/leaflet/blocks/signup.json b/lexicons/pub/leaflet/blocks/signup.json new file mode 100644 index 00000000..b5a3f594 --- /dev/null +++ b/lexicons/pub/leaflet/blocks/signup.json @@ -0,0 +1,12 @@ +{ + "lexicon": 1, + "id": "pub.leaflet.blocks.signup", + "defs": { + "main": { + "type": "object", + "description": "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", + "required": [], + "properties": {} + } + } +} \ No newline at end of file diff --git a/lexicons/pub/leaflet/pages/canvas.json b/lexicons/pub/leaflet/pages/canvas.json index 275a367e..fb465f90 100644 --- a/lexicons/pub/leaflet/pages/canvas.json +++ b/lexicons/pub/leaflet/pages/canvas.json @@ -48,7 +48,8 @@ "pub.leaflet.blocks.page", "pub.leaflet.blocks.poll", "pub.leaflet.blocks.button", - "pub.leaflet.blocks.postsList" + "pub.leaflet.blocks.postsList", + "pub.leaflet.blocks.signup" ] }, "x": { diff --git a/lexicons/pub/leaflet/pages/linearDocument.json b/lexicons/pub/leaflet/pages/linearDocument.json index 29e5e200..399c886c 100644 --- a/lexicons/pub/leaflet/pages/linearDocument.json +++ b/lexicons/pub/leaflet/pages/linearDocument.json @@ -45,7 +45,8 @@ "pub.leaflet.blocks.page", "pub.leaflet.blocks.poll", "pub.leaflet.blocks.button", - "pub.leaflet.blocks.postsList" + "pub.leaflet.blocks.postsList", + "pub.leaflet.blocks.signup" ] }, "alignment": { diff --git a/lexicons/src/blocks.ts b/lexicons/src/blocks.ts index 28baef5c..746c73e8 100644 --- a/lexicons/src/blocks.ts +++ b/lexicons/src/blocks.ts @@ -382,6 +382,20 @@ export const PubLeafletBlocksPostsList: LexiconDoc = { }, }; +export const PubLeafletBlocksSignup: LexiconDoc = { + lexicon: 1, + id: "pub.leaflet.blocks.signup", + defs: { + main: { + type: "object", + description: + "A subscribe/signup form for the publication. Renders the publication's subscribe form; carries no configurable data.", + required: [], + properties: {}, + }, + }, +}; + export const BlockLexicons = [ PubLeafletBlocksIFrame, PubLeafletBlocksText, @@ -400,6 +414,7 @@ export const BlockLexicons = [ PubLeafletBlocksPoll, PubLeafletBlocksButton, PubLeafletBlocksPostsList, + PubLeafletBlocksSignup, ]; export const BlockUnion: LexRefUnion = { type: "union", diff --git a/src/replicache/attributes.ts b/src/replicache/attributes.ts index a3b0a0a3..142764ba 100644 --- a/src/replicache/attributes.ts +++ b/src/replicache/attributes.ts @@ -403,7 +403,8 @@ export type Data = { | "code" | "blockquote" | "horizontal-rule" - | "posts-list"; + | "posts-list" + | "signup"; }; "canvas-pattern-union": { type: "canvas-pattern-union"; diff --git a/src/utils/factsToPagesRecord.ts b/src/utils/factsToPagesRecord.ts index dbc251fb..03638c5c 100644 --- a/src/utils/factsToPagesRecord.ts +++ b/src/utils/factsToPagesRecord.ts @@ -18,6 +18,7 @@ import { PubLeafletBlocksPage, PubLeafletBlocksPoll, PubLeafletBlocksPostsList, + PubLeafletBlocksSignup, PubLeafletBlocksText, PubLeafletBlocksUnorderedList, PubLeafletBlocksWebsite, @@ -569,6 +570,12 @@ export async function processBlocksToPages(opts: { }; return block; } + if (b.type === "signup") { + const block: $Typed = { + $type: ids.PubLeafletBlocksSignup, + }; + return block; + } return; } diff --git a/src/utils/getBlocksAsHTML.tsx b/src/utils/getBlocksAsHTML.tsx index c205dc08..d3559681 100644 --- a/src/utils/getBlocksAsHTML.tsx +++ b/src/utils/getBlocksAsHTML.tsx @@ -88,6 +88,7 @@ const BlockTypeToHTML: { mailbox: async () => null, poll: async () => null, embed: async () => null, + signup: async () => null, "bluesky-post": async (b, tx) => { let [post] = await scanIndex(tx).eav(b.value, "block/bluesky-post"); if (!post) return null;