{props.bskyPostText}
diff --git a/app/(home-pages)/notifications/Notification.tsx b/app/(home-pages)/notifications/Notification.tsx
index 205d15a8..62fbc905 100644
--- a/app/(home-pages)/notifications/Notification.tsx
+++ b/app/(home-pages)/notifications/Notification.tsx
@@ -23,14 +23,9 @@ export const Notification = (props: {
+
);
return (
-
-
+
+
+ Notifications
+
{notifications.map((n) => {
if (n.type === "comment") {
if (n.parentData) return ;
diff --git a/app/(home-pages)/notifications/QuoteNotification.tsx b/app/(home-pages)/notifications/QuoteNotification.tsx
index 6c7b3f16..27335ad7 100644
--- a/app/(home-pages)/notifications/QuoteNotification.tsx
+++ b/app/(home-pages)/notifications/QuoteNotification.tsx
@@ -27,12 +27,9 @@ export const QuoteNotification = (props: HydratedQuoteNotification) => {
content={
-
+
{postText}
diff --git a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx
index 7bd67c3c..b5022e1c 100644
--- a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx
+++ b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx
@@ -1,5 +1,6 @@
"use client";
+import { EmptyState } from "components/EmptyState";
import { PostListing } from "components/PostListing";
import type { Post } from "app/(home-pages)/reader/getReaderFeed";
import type { Cursor } from "./getProfilePosts";
@@ -64,7 +65,7 @@ export const ProfilePostsContent = (props: {
const allPosts = data ? data.flatMap((page) => page.posts) : [];
if (allPosts.length === 0 && !isValidating) {
- return No posts yet;
+ return ;
}
return (
diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
index 3589e780..56627e9d 100644
--- a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
+++ b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
@@ -45,7 +45,7 @@ export const ProfileHeader = (props: {
return (
{!props.popover && }
diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
index 15e598d0..df27af0e 100644
--- a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
+++ b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx
@@ -10,10 +10,10 @@ export function ProfileLayout(props: { children: React.ReactNode }) {
className={`
${
cardBorderHidden
- ? ""
- : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page px-3 sm:px-4"
+ ? "bg-transparent"
+ : "overflow-y-scroll h-full border border-border-light rounded-lg! container px-3 sm:px-4 sm:-mt-2"
}
- max-w-prose mx-auto w-full
+ max-w-prose w-full
flex flex-col pb-3
text-center
`}
diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
index bb058749..e43e6286 100644
--- a/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
+++ b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
@@ -41,7 +41,9 @@ export const ProfileTabs = (props: { didOrHandle: string }) => {
const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)";
return (
-
+
{
scrollPosWithinTabContent < 20
? {
backgroundColor: !cardBorderHidden
- ? `rgba(${bgColor}, ${scrollPosWithinTabContent / 60 + 0.75})`
+ ? `rgba(${bgColor}, ${scrollPosWithinTabContent / 60})`
: `rgba(${bgColor}, ${scrollPosWithinTabContent / 20})`,
paddingLeft: !cardBorderHidden
? "4px"
@@ -110,7 +112,7 @@ const TabLink = (props: { href: string; name: string; selected: boolean }) => {
className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer hover:no-underline! ${
props.selected
? "text-accent-2 bg-accent-1 font-bold -mb-px"
- : "text-tertiary"
+ : "text-secondary"
}`}
>
{props.name}
diff --git a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
index a2a3f474..39b183ac 100644
--- a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
+++ b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
@@ -1,5 +1,6 @@
"use client";
+import { EmptyState } from "components/EmptyState";
import { useEffect, useRef, useMemo } from "react";
import useSWRInfinite from "swr/infinite";
import { AppBskyActorProfile, AtUri } from "@atproto/api";
@@ -79,9 +80,7 @@ export const ProfileCommentsContent = (props: {
const allComments = data ? data.flatMap((page) => page.comments) : [];
if (allComments.length === 0 && !isValidating) {
- return (
- No comments yet
- );
+ return ;
}
return (
@@ -206,7 +205,7 @@ const CommentItem = ({ comment }: { comment: ProfileComment }) => {
)}
No subscriptions yet
- );
+ return ;
}
return (
diff --git a/app/(home-pages)/reader/GlobalContent.tsx b/app/(home-pages)/reader/GlobalContent.tsx
index 94134cfe..31ae8004 100644
--- a/app/(home-pages)/reader/GlobalContent.tsx
+++ b/app/(home-pages)/reader/GlobalContent.tsx
@@ -2,6 +2,7 @@
import { use } from "react";
import useSWR from "swr";
import { callRPC } from "app/api/rpc/client";
+import { EmptyState } from "components/EmptyState";
import { PostListing } from "components/PostListing";
import type { Post } from "./getReaderFeed";
import {
@@ -33,11 +34,7 @@ export const GlobalContent = (props: {
let selectedPost = useSelectedPostListing((s) => s.selectedPostListing);
if (posts.length === 0) {
- return (
-
- Nothing trending right now. Check back soon!
-
- );
+ return ;
}
return (
diff --git a/app/(home-pages)/reader/NewContent.tsx b/app/(home-pages)/reader/NewContent.tsx
index 9a0e7296..0c8d477b 100644
--- a/app/(home-pages)/reader/NewContent.tsx
+++ b/app/(home-pages)/reader/NewContent.tsx
@@ -1,6 +1,7 @@
"use client";
import { use } from "react";
+import { EmptyState } from "components/EmptyState";
import type { Cursor, Post } from "./getReaderFeed";
import useSWRInfinite from "swr/infinite";
import { getNewFeed } from "./getNewFeed";
@@ -65,11 +66,7 @@ export const NewContent = (props: {
const allPosts = data ? data.flatMap((page) => page.posts) : [];
if (allPosts.length === 0) {
- return (
-
- No posts yet. Check back soon!
-
- );
+ return ;
}
return (
diff --git a/app/(home-pages)/reader/ReaderMentionsContent.tsx b/app/(home-pages)/reader/ReaderMentionsContent.tsx
index 517e6be0..b379075a 100644
--- a/app/(home-pages)/reader/ReaderMentionsContent.tsx
+++ b/app/(home-pages)/reader/ReaderMentionsContent.tsx
@@ -2,6 +2,7 @@
import useSWR from "swr";
import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs";
import { BskyPostContent } from "app/lish/[did]/[publication]/[rkey]/BskyPostContent";
+import { EmptyState } from "components/EmptyState";
import { DotLoader } from "components/utils/DotLoader";
async function fetchBskyPosts(uris: string[]): Promise {
@@ -17,9 +18,10 @@ export function ReaderMentionsContent(props: {
quotesAndMentions: { uri: string; link?: string }[];
}) {
const uris = props.quotesAndMentions.map((q) => q.uri);
- const key = uris.length > 0
- ? `/api/bsky/hydrate?${new URLSearchParams({ uris: JSON.stringify(uris) }).toString()}`
- : null;
+ const key =
+ uris.length > 0
+ ? `/api/bsky/hydrate?${new URLSearchParams({ uris: JSON.stringify(uris) }).toString()}`
+ : null;
const { data: bskyPosts, isLoading } = useSWR(key, () =>
fetchBskyPosts(uris),
@@ -27,9 +29,11 @@ export function ReaderMentionsContent(props: {
if (props.quotesAndMentions.length === 0) {
return (
-
- no mentions yet!
-
+
);
}
diff --git a/app/(home-pages)/reader/layout.tsx b/app/(home-pages)/reader/layout.tsx
index 36b65237..58d41c1c 100644
--- a/app/(home-pages)/reader/layout.tsx
+++ b/app/(home-pages)/reader/layout.tsx
@@ -57,7 +57,7 @@ export default function ReaderLayout({
className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${
isActive(tab.href)
? "text-accent-2 bg-accent-1 font-bold -mb-px"
- : "text-tertiary"
+ : "text-secondary"
}`}
>
{tab.name}
diff --git a/app/(home-pages)/tag/[tag]/page.tsx b/app/(home-pages)/tag/[tag]/page.tsx
index 2121ab72..84c6a9e8 100644
--- a/app/(home-pages)/tag/[tag]/page.tsx
+++ b/app/(home-pages)/tag/[tag]/page.tsx
@@ -1,8 +1,6 @@
import { DashboardLayout } from "components/PageLayouts/DashboardLayout";
-import { Tag } from "components/Tags";
import { PostListing } from "components/PostListing";
import { getDocumentsByTag } from "./getDocumentsByTag";
-import { TagTiny } from "components/Icons/TagTiny";
import { Metadata } from "next";
export async function generateMetadata(props: {
@@ -41,38 +39,28 @@ const TagContent = (props: {
posts: Awaited>["posts"];
}) => {
return (
-
-
-
-
-
+
+ Tag: {props.tag}
+
+
{props.posts.length === 0 ? (
-
+
) : (
- props.posts.map((post) => (
-
- ))
+ <>
+
+ {props.posts.length} {props.posts.length === 1 ? "post" : "posts"}
+
+ {props.posts.map((post) => (
+
+ ))}
+ >
)}
);
};
-const TagHeader = (props: { tag: string; postCount: number }) => {
- return (
-
-
-
- {props.tag}
-
-
- {props.postCount} {props.postCount === 1 ? "post" : "posts"}
-
-
- );
-};
-
-const EmptyState = (props: { tag: string }) => {
+const NoPostsForTag = (props: { tag: string }) => {
return (
diff --git a/app/globals.css b/app/globals.css
index 80de871f..7573c319 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -473,13 +473,20 @@ pre.shiki {
@apply ml-2;
}
+.container {
+ @apply border;
+ @apply border-border-light;
+ @apply rounded-md;
+ background: rgba(var(--bg-page), var(--bg-page-alpha));
+}
+
.transparent-container {
@apply border;
@apply border-border-light;
@apply rounded-md;
}
-.container {
+.frosted-container {
background: rgba(var(--bg-page), 0.75);
@apply border;
@apply border-bg-page;
diff --git a/app/lish/[did]/[publication]/PublicationContent.tsx b/app/lish/[did]/[publication]/PublicationContent.tsx
new file mode 100644
index 00000000..982870ec
--- /dev/null
+++ b/app/lish/[did]/[publication]/PublicationContent.tsx
@@ -0,0 +1,253 @@
+import React from "react";
+import { AtUri } from "@atproto/syntax";
+import {
+ getPublicationURL,
+ getDocumentURL,
+} from "app/lish/createPub/getPublicationURL";
+import { SubscribeWithBluesky } from "app/lish/Subscribe";
+import { InteractionPreview } from "components/InteractionsPreview";
+import { LocalizedDate } from "./LocalizedDate";
+import { PublicationHomeLayout } from "./PublicationHomeLayout";
+import { PublicationAuthor } from "./PublicationAuthor";
+import {
+ normalizePublicationRecord,
+ normalizeDocumentRecord,
+} from "src/utils/normalizeRecords";
+import { getFirstParagraph } from "src/utils/getFirstParagraph";
+import { FontLoader } from "components/FontLoader";
+import { SpeedyLink } from "components/SpeedyLink";
+
+type FakePost = {
+ title: string;
+ description: string;
+ date: React.ReactNode;
+};
+
+export const PublicationContent = ({
+ record,
+ publication,
+ did,
+ profile,
+ showPageBackground,
+ fakePosts,
+}: {
+ record: ReturnType;
+ publication: {
+ uri: string;
+ name: string;
+ identity_did: string;
+ record: unknown;
+ publication_subscriptions: { identity: string }[];
+ documents_in_publications: {
+ documents: {
+ uri: string;
+ data: unknown;
+ comments_on_documents: { count: number }[];
+ document_mentions_in_bsky: { count: number }[];
+ recommends_on_documents: { count: number }[];
+ } | null;
+ }[];
+ };
+ did: string;
+ profile: { did: string; displayName?: string; handle: string } | undefined;
+ showPageBackground: boolean | undefined;
+ fakePosts?: FakePost[];
+}) => {
+ return (
+ <>
+
+
+
+ ) : undefined
+ }
+ subscribeButton={
+
+ }
+ />
+
+ {fakePosts &&
+ fakePosts.map((post, i) => (
+
+ ))}
+ {!fakePosts &&
+ publication.documents_in_publications
+ .filter((d) => !!d?.documents)
+ .sort((a, b) => {
+ const aRecord = normalizeDocumentRecord(a.documents?.data);
+ const bRecord = normalizeDocumentRecord(b.documents?.data);
+ const aDate = aRecord?.publishedAt
+ ? new Date(aRecord.publishedAt)
+ : new Date(0);
+ const bDate = bRecord?.publishedAt
+ ? new Date(bRecord.publishedAt)
+ : new Date(0);
+ return bDate.getTime() - aDate.getTime(); // Sort by most recent first
+ })
+ .map((doc) => {
+ if (!doc.documents) return null;
+ const doc_record = normalizeDocumentRecord(doc.documents.data);
+ if (!doc_record) return null;
+ let uri = new AtUri(doc.documents.uri);
+ let quotes =
+ doc.documents.document_mentions_in_bsky[0].count || 0;
+ let comments =
+ record?.preferences?.showComments === false
+ ? 0
+ : doc.documents.comments_on_documents[0].count || 0;
+ let recommends =
+ doc.documents.recommends_on_documents?.[0]?.count || 0;
+ let tags = doc_record.tags || [];
+
+ const docUrl = getDocumentURL(
+ doc_record,
+ doc.documents.uri,
+ publication,
+ );
+ return (
+
+
+ ) : undefined
+ }
+ interactions={
+
+ }
+ />
+
+ );
+ })}
+
+
+ >
+ );
+};
+
+export function PublicationHeader(props: {
+ iconUrl?: string;
+ publicationName: string;
+ description?: string;
+ author?: React.ReactNode;
+ subscribeButton?: React.ReactNode;
+}) {
+ return (
+
+ {props.iconUrl && (
+
+ )}
+
+ {props.publicationName}
+
+ {props.description}
+ {props.author}
+ {props.subscribeButton}
+
+ );
+}
+
+export function PublicationPostItem(props: {
+ href?: string;
+ title?: string;
+ description?: string;
+ date?: React.ReactNode;
+ interactions?: React.ReactNode;
+}) {
+ const content = (
+ <>
+ {props.title && {props.title}
}
+ {props.description}
+ >
+ );
+
+ return (
+ <>
+
+ {props.href ? (
+
+ {content}
+
+ ) : (
+
+ {content}
+
+ )}
+
+
+ {props.date}
+ {props.interactions}
+
+
+
+ >
+ );
+}
diff --git a/app/lish/[did]/[publication]/PublicationHomeLayout.tsx b/app/lish/[did]/[publication]/PublicationHomeLayout.tsx
index 621119ff..2a2e9787 100644
--- a/app/lish/[did]/[publication]/PublicationHomeLayout.tsx
+++ b/app/lish/[did]/[publication]/PublicationHomeLayout.tsx
@@ -11,11 +11,11 @@ export function PublicationHomeLayout(props: {
return (
{props.children}
diff --git a/app/lish/[did]/[publication]/UpgradeModal.tsx b/app/lish/[did]/[publication]/UpgradeModal.tsx
index be28a42c..daa12094 100644
--- a/app/lish/[did]/[publication]/UpgradeModal.tsx
+++ b/app/lish/[did]/[publication]/UpgradeModal.tsx
@@ -3,6 +3,7 @@ import { Modal } from "components/Modal";
import { useState } from "react";
import { createCheckoutSession } from "actions/createCheckoutSession";
import { DotLoader } from "components/utils/DotLoader";
+import { ToggleGroup } from "components/ToggleGroup";
export const UpgradeContent = () => {
let [cadence, setCadence] = useState<"year" | "month">("year");
@@ -37,20 +38,15 @@ export const UpgradeContent = () => {
-
-
-
-
+
{cadence === "year" ? "$120" : "$12"}
@@ -90,20 +86,36 @@ export const UpgradeModal = (props: {
);
};
-export const InlineUpgrade = () => {
+export const InlineUpgradeToPro = (props: { compact?: boolean }) => {
return (
-
-
- Upgrade to Leaflet Pro!
-
- }
- />
-
+
+
+
Analytics for all your pubs!
Emails and membership coming soon.
);
};
+
+export const UpgradeToProButton = (props: {
+ fullWidth?: boolean;
+ compact?: boolean;
+}) => {
+ return (
+
+ Upgrade to Leaflet Pro!
+
+ }
+ />
+ );
+};
diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
index a89f07ec..bda5e413 100644
--- a/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
+++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
@@ -134,7 +134,9 @@ export const Interactions = (props: {
const tagCount = tags?.length || 0;
let interactionsAvailable =
- props.showComments || props.showMentions || props.showRecommends;
+ props.showComments ||
+ (props.showMentions && props.quotesCount > 0) ||
+ props.showRecommends;
return (
+
) : undefined
}
>
diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx
index f7b39074..81cc2a85 100644
--- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx
+++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx
@@ -69,7 +69,7 @@ export function PostContent({
return (
{blocks.map((b, index) => {
return (
@@ -162,10 +162,12 @@ export let Block = ({
)
alignment = "text-center justify-center";
+ let isHeading = PubLeafletBlocksHeader.isMain(b.block);
+
let className = `
postBlockWrapper
min-h-7
- mt-1 mb-2
+ ${isFirst ? "mt-0" : "mt-1"} ${isLast ? "mb-3 sm:mb-4" : isHeading ? "mb-0!" : "mb-2"}
${isList && "isListItem mb-0! "}
${alignment}
`;
@@ -390,7 +392,7 @@ export let Block = ({
return (
// all this margin stuff is a highly unfortunate hack so that the border-l on blockquote is the height of just the text rather than the height of the block, which includes padding.
{props.postTitle && (
-
+
{props.postTitle}
-
+
)}
{props.postDescription ? (
diff --git a/app/lish/[did]/[publication]/dashboard/Actions.tsx b/app/lish/[did]/[publication]/dashboard/Actions.tsx
index f6c8b01c..4a601d4f 100644
--- a/app/lish/[did]/[publication]/dashboard/Actions.tsx
+++ b/app/lish/[did]/[publication]/dashboard/Actions.tsx
@@ -1,7 +1,6 @@
"use client";
import { NewDraftActionButton } from "./NewDraftButton";
-import { PublicationSettingsButton } from "./settings/PublicationSettings";
import { ActionButton } from "components/ActionBar/ActionButton";
import { ShareSmall } from "components/Icons/ShareSmall";
import { Menu, MenuItem } from "components/Menu";
@@ -22,7 +21,6 @@ export const Actions = (props: { publication: string }) => {
<>
-
>
);
};
diff --git a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
index 3da89926..61510f54 100644
--- a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
+++ b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
@@ -14,6 +14,8 @@ import { useDebouncedEffect } from "src/hooks/useDebouncedEffect";
import { type NormalizedPublication } from "src/utils/normalizeRecords";
import { PublicationAnalytics } from "./PublicationAnalytics";
import { useCanSeePro } from "src/hooks/useEntitlement";
+import { SettingsContent } from "./settings/SettingsContent";
+import { SettingsTiny } from "components/Icons/SettingsTiny";
export default function PublicationDashboard({
publication,
@@ -67,7 +69,7 @@ export default function PublicationDashboard({
),
controls: null,
},
- Subscribers: {
+ Subs: {
content: (
,
+ content: (
+
+ ),
+ controls: null,
+ },
}}
actions={ }
currentPage="pub"
diff --git a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
index 95583a32..e6deccc6 100644
--- a/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
+++ b/app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
@@ -1,6 +1,7 @@
"use client";
import { AtUri } from "@atproto/syntax";
import { EditTiny } from "components/Icons/EditTiny";
+import { EmptyState } from "components/EmptyState";
import {
usePublicationData,
@@ -28,11 +29,7 @@ export function PublishedPostsList(props: {
const pubRecord = useNormalizedPublicationRecord();
if (!publication) return null;
if (!documents || documents.length === 0)
- return (
-
- Nothing's been published yet...
-
- );
+ return ;
// Sort by publishedAt (most recent first)
const sortedDocuments = [...documents].sort((a, b) => {
@@ -89,7 +86,9 @@ function PublishedPostItem(props: {
- {doc.record.title}
+ {doc.record.title === "" || doc.record.title === undefined
+ ? "Untitled"
+ : doc.record.title}
diff --git a/app/lish/[did]/[publication]/dashboard/settings/GeneralSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/GeneralSettings.tsx
new file mode 100644
index 00000000..2d606bc9
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/GeneralSettings.tsx
@@ -0,0 +1,94 @@
+import { useRef } from "react";
+import { Input } from "components/Input";
+import { AddTiny } from "components/Icons/AddTiny";
+import { DashboardContainer } from "./SettingsContent";
+import { EditTiny } from "components/Icons/EditTiny";
+
+export function GeneralSettings(props: {
+ nameValue: string;
+ setNameValue: (v: string) => void;
+ descriptionValue: string;
+ setDescriptionValue: (v: string) => void;
+ iconPreview: string | null;
+ setIconPreview: (v: string | null) => void;
+ setIconFile: (f: File | null) => void;
+}) {
+ let fileInputRef = useRef(null);
+
+ return (
+
+
+
+
+ Logo (optional)
+
+
+ {props.iconPreview && (
+ fileInputRef.current?.click()}
+ >
+
+
+ )}
+ fileInputRef.current?.click()}
+ >
+ {props.iconPreview ? (
+
+ ) : (
+
+ )}
+
+
+ {
+ const file = e.target.files?.[0];
+ if (file) {
+ props.setIconFile(file);
+ const reader = new FileReader();
+ reader.onload = (ev) => {
+ props.setIconPreview(ev.target?.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx b/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx
index 52615c45..a8db0cc8 100644
--- a/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx
+++ b/app/lish/[did]/[publication]/dashboard/settings/ManageProSubscription.tsx
@@ -7,7 +7,7 @@ import { useLocalizedDate } from "src/hooks/useLocalizedDate";
import { GoBackSmall } from "components/Icons/GoBackSmall";
import { PRODUCT_DEFINITION } from "stripe/products";
-export const ManageProSubscription = (props: {}) => {
+export const ManageProSubscription = (props: { compact?: boolean }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { identity } = useIdentityData();
@@ -29,27 +29,49 @@ export const ManageProSubscription = (props: {}) => {
setLoading(false);
}
}
+ if (props.compact) {
+ return (
+
+
+ {loading ? : "Manage Billing"}
+
+
+ {subscription?.status === "canceled"
+ ? "Your subscription has ended"
+ : subscription?.status === "canceling"
+ ? `Access until ${renewalDate}`
+ : `Renews ${renewalDate}`}
+
+ {error && {error}}
+
+ );
+ }
return (
-
- Manage Subscription
-
-
+
- You have a
- {PRODUCT_DEFINITION.name} subscription
-
+ You are subscribed to
+
+
{PRODUCT_DEFINITION.name}
- {subscription?.status === "canceled"
- ? "Your subscription has ended"
- : subscription?.status === "canceling"
- ? `Access until ${renewalDate}`
- : `Renews on ${renewalDate}`}
+
+ {subscription?.status === "canceled"
+ ? "Your subscription has ended"
+ : subscription?.status === "canceling"
+ ? `Access until ${renewalDate}`
+ : `Renews ${renewalDate}`}
+
void;
- loading: boolean;
- setLoading: (l: boolean) => void;
-}) => {
- let { data } = usePublicationData();
-
- let { publication: pubData } = data || {};
- const record = useNormalizedPublicationRecord();
-
- let [showComments, setShowComments] = useState(
- record?.preferences?.showComments === undefined
- ? true
- : record.preferences.showComments,
- );
- let [showMentions, setShowMentions] = useState(
- record?.preferences?.showMentions === undefined
- ? true
- : record.preferences.showMentions,
- );
- let [showRecommends, setShowRecommends] = useState(
- record?.preferences?.showRecommends === undefined
- ? true
- : record.preferences.showRecommends,
- );
- let [showPrevNext, setShowPrevNext] = useState(
- record?.preferences?.showPrevNext === undefined
- ? true
- : record.preferences.showPrevNext,
- );
-
- let toast = useToaster();
- return (
-
- );
-};
diff --git a/app/lish/[did]/[publication]/dashboard/settings/PostSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/PostSettings.tsx
new file mode 100644
index 00000000..94e4962f
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/PostSettings.tsx
@@ -0,0 +1,83 @@
+import { Toggle } from "components/Toggle";
+import { DashboardContainer } from "./SettingsContent";
+
+export function PostSettings(props: {
+ showComments: boolean;
+ setShowComments: (v: boolean) => void;
+ showMentions: boolean;
+ setShowMentions: (v: boolean) => void;
+ showRecommends: boolean;
+ setShowRecommends: (v: boolean) => void;
+ showPrevNext: boolean;
+ setShowPrevNext: (v: boolean) => void;
+ showInDiscover: boolean;
+ setShowInDiscover: (v: boolean) => void;
+}) {
+ return (
+ <>
+
+ props.setShowInDiscover(!props.showInDiscover)}
+ >
+
+ Make Public
+
+ Your posts will appear in{" "}
+
+ Leaflet Reader
+ {" "}
+ and show up in search and tags.
+
+
+
+
+
+
+ props.setShowPrevNext(!props.showPrevNext)}
+ >
+
+ Show Prev/Next Buttons on Post
+
+
+
+
+
+
+ props.setShowComments(!props.showComments)}
+ >
+ Show Comments
+
+
+ props.setShowMentions(!props.showMentions)}
+ >
+
+ Show Mentions
+
+ Display a list of Bluesky mentions about your post
+
+
+
+
+ props.setShowRecommends(!props.showRecommends)}
+ >
+
+ Show Recommends
+
+ Allow readers to recommend/like your post
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/lish/[did]/[publication]/dashboard/settings/PubDomainSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/PubDomainSettings.tsx
new file mode 100644
index 00000000..78ac94f5
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/PubDomainSettings.tsx
@@ -0,0 +1,374 @@
+"use client";
+
+import { useState } from "react";
+import { mutate } from "swr";
+import { useDomainStatus } from "components/Domains/useDomainStatus";
+import {
+ getDomainAssignment,
+ describeAssignment,
+} from "components/Domains/domainAssignment";
+import {
+ useIdentityData,
+ mutateIdentityData,
+} from "components/IdentityProvider";
+import { ButtonPrimary } from "components/Buttons";
+import {
+ usePublicationData,
+ useNormalizedPublicationRecord,
+} from "../PublicationSWRProvider";
+import { updatePublicationBasePath } from "app/lish/createPub/updatePublication";
+import {
+ assignDomainToPublication,
+ removeDomainAssignment,
+} from "actions/domains";
+import { PinTiny } from "components/Icons/PinTiny";
+import { LoadingTiny } from "components/Icons/LoadingTiny";
+import { UnlinkTiny } from "components/Icons/UnlinkTiny";
+import { DotLoader } from "components/utils/DotLoader";
+import { useToaster } from "components/Toast";
+import type { CustomDomain } from "components/Domains/DomainList";
+
+export const PubDomainSettings = () => {
+ let { data, mutate: mutatePubData } = usePublicationData();
+ let { publication: pubData } = data || {};
+ let record = useNormalizedPublicationRecord();
+ let { identity, mutate: mutateIdentity } = useIdentityData();
+ let toaster = useToaster();
+ let basePath = record?.url?.replace(/^https?:\/\//, "") || "";
+
+ let pubDomains = pubData?.publication_domains || [];
+ let pubDomainNames = new Set(pubDomains.map((d) => d.domain));
+
+ if (!pubData) return null;
+
+ return (
+ <>
+
+ This Publication's Domains
+ DEFAULT
+ {pubDomains
+ .filter((d) => d.domain === basePath)
+ .map((d) => (
+
+ ))}
+ {pubDomains.filter((d) => d.domain !== basePath).length !== 0 && (
+ <>
+ ALTERNATES
+ {pubDomains
+ .filter((d) => d.domain !== basePath)
+ .map((d) => (
+
+ ))}
+ >
+ )}
+
+
+ Available Domains
+ {(() => {
+ let availableDomains = (identity?.custom_domains || [])
+ .filter((d) => !pubDomainNames.has(d.domain))
+ .filter(
+ (d) =>
+ d.publication_domains.length === 0 &&
+ d.custom_domain_routes.length === 0,
+ );
+ return availableDomains.length > 0 ? (
+ <>
+ {availableDomains.map((d) => (
+ {
+ mutateIdentity();
+ mutate("publication-data");
+ }}
+ />
+ ))}
+
+ Add new domains from your profile settings!
+
+ >
+ ) : (
+
+ No available domains!
+
+ Add new domains from your profile settings!
+
+ );
+ })()}
+
+ >
+ );
+};
+
+function PubDomainRow(props: {
+ domain: string;
+ publication_uri: string;
+ basePath: string;
+ mutatePubData: ReturnType["mutate"];
+ mutateIdentity: ReturnType["mutate"];
+ toaster: ReturnType;
+}) {
+ let { pending } = useDomainStatus(props.domain);
+ let [loading, setLoading] = useState(false);
+ let [unlinking, setUnlinking] = useState(false);
+ let toaster = props.toaster;
+
+ return (
+
+ {props.domain}
+
+ {pending ? (
+
+
+ pending
+
+
+
+ ) : (
+ <>
+ {props.basePath !== props.domain && (
+
+ {!props.domain.endsWith(".leaflet.pub") && (
+
+ )}
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
+
+function UnassignedDomainRow(props: {
+ domainData: CustomDomain;
+ publication_uri: string;
+ mutatePubData: ReturnType["mutate"];
+ onAssigned: () => void;
+}) {
+ let { pending } = useDomainStatus(props.domainData.domain);
+ let { mutate: mutateIdentity } = useIdentityData();
+ let assignment = getDomainAssignment(props.domainData);
+ let [confirming, setConfirming] = useState(false);
+ let [loading, setLoading] = useState(false);
+
+ async function doAssign() {
+ setLoading(true);
+ try {
+ mutateIdentityData(mutateIdentity, (draft) => {
+ let domain = draft.custom_domains.find(
+ (d) => d.domain === props.domainData.domain,
+ );
+ if (domain) {
+ domain.custom_domain_routes = [];
+ let pub = draft.publications?.find(
+ (p) => p.uri === props.publication_uri,
+ );
+ domain.publication_domains = [
+ {
+ publication: props.publication_uri,
+ domain: props.domainData.domain,
+ identity: "",
+ created_at: new Date().toISOString(),
+ publications: pub ? { name: pub.name } : null,
+ },
+ ];
+ }
+ });
+ props.mutatePubData(
+ (current) => {
+ if (!current) return current;
+ let pub = current.publication;
+ if (!pub) return current;
+ return {
+ ...current,
+ publication: {
+ ...pub,
+ publication_domains: [
+ ...(pub.publication_domains || []),
+ {
+ publication: props.publication_uri,
+ domain: props.domainData.domain,
+ created_at: new Date().toISOString(),
+ identity: "",
+ },
+ ],
+ },
+ };
+ },
+ { revalidate: false },
+ );
+ setConfirming(false);
+ props.onAssigned();
+ await assignDomainToPublication({
+ domain: props.domainData.domain,
+ publication_uri: props.publication_uri,
+ });
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+ {props.domainData.domain}
+ {pending ? (
+ unverified
+ ) : confirming ? null : (
+
+ )}
+
+ {confirming && (
+
+
+ This domain is currently assigned to{" "}
+ {describeAssignment(assignment)}. Assigning it here will remove that
+ assignment.
+
+
+
+
+ {loading ? : "Reassign"}
+
+
+
+ )}
+
+ );
+}
diff --git a/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
deleted file mode 100644
index 4d58d068..00000000
--- a/app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-"use client";
-
-import { ActionButton } from "components/ActionBar/ActionButton";
-import { Popover } from "components/Popover";
-import { SettingsSmall } from "components/Icons/SettingsSmall";
-import { EditPubForm } from "app/lish/createPub/UpdatePubForm";
-import { PubThemeSetter } from "components/ThemeManager/PubThemeSetter";
-import { useIsMobile } from "src/hooks/isMobile";
-import { useState } from "react";
-import { GoBackSmall } from "components/Icons/GoBackSmall";
-import { theme } from "tailwind.config";
-import { ButtonPrimary } from "components/Buttons";
-import { DotLoader } from "components/utils/DotLoader";
-import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
-import { PostOptions } from "./PostOptions";
-import { PublicationDomains } from "components/Domains/PublicationDomains";
-import { usePublicationData } from "../PublicationSWRProvider";
-
-type menuState = "menu" | "pub-settings" | "theme" | "post-settings" | "domains";
-
-export function PublicationSettingsButton(props: { publication: string }) {
- let isMobile = useIsMobile();
- let [state, setState] = useState("menu");
- let [loading, setLoading] = useState(false);
-
- return (
- setState("menu")}
- side={isMobile ? "top" : "right"}
- align={isMobile ? "center" : "start"}
- className={`flex flex-col max-w-xs w-[1000px] ${state === "theme" && "bg-white!"} pb-0!`}
- arrowFill={theme.colors["border-light"]}
- trigger={
-
- label="Settings"
- smallOnMobile
- />
- }
- >
- {state === "pub-settings" ? (
- setState("menu")}
- loading={loading}
- setLoadingAction={setLoading}
- />
- ) : state === "theme" ? (
- setState("menu")}
- loading={loading}
- setLoading={setLoading}
- />
- ) : state === "post-settings" ? (
- setState("menu")}
- loading={loading}
- setLoading={setLoading}
- />
- ) : state === "domains" ? (
- setState("menu")} />
- ) : (
-
- )}
-
-
- );
-}
-
-const PubSettingsMenu = (props: {
- state: menuState;
- setState: (s: menuState) => void;
- loading: boolean;
- setLoading: (l: boolean) => void;
-}) => {
- let menuItemClassName =
- "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!";
-
- return (
-
- Settings
-
-
-
-
-
- );
-};
-
-function PublicationDomainsView(props: { backToMenu: () => void }) {
- let { data } = usePublicationData();
- let { publication: pubData } = data || {};
- if (!pubData) return null;
- return (
-
- );
-}
-
-export const PubSettingsHeader = (props: {
- backToMenuAction?: () => void;
- loading?: boolean;
- setLoadingAction?: (l: boolean) => void;
- children: React.ReactNode;
-}) => {
- return (
-
- {props.children}
- {props.backToMenuAction && (
-
-
- {props.setLoadingAction && (
-
- {props.loading ? : "Update"}
-
- )}
-
- )}
-
- );
-};
diff --git a/app/lish/[did]/[publication]/dashboard/settings/SettingsContent.tsx b/app/lish/[did]/[publication]/dashboard/settings/SettingsContent.tsx
new file mode 100644
index 00000000..7127f352
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/SettingsContent.tsx
@@ -0,0 +1,373 @@
+"use client";
+
+import { useState, useEffect, useMemo } from "react";
+import { ButtonPrimary, ButtonSecondary } from "components/Buttons";
+import { DotLoader } from "components/utils/DotLoader";
+import { useToaster } from "components/Toast";
+import { mutate } from "swr";
+import {
+ usePublicationData,
+ useNormalizedPublicationRecord,
+} from "../PublicationSWRProvider";
+import { updatePublication } from "app/lish/createPub/updatePublication";
+import { PubDomainSettings } from "./PubDomainSettings";
+import { GeneralSettings } from "./GeneralSettings";
+import { PostSettings } from "./PostSettings";
+import { ThemeSettings } from "./ThemeSettings";
+import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
+import { ManageProSubscription } from "./ManageProSubscription";
+import { useIsPro, useCanSeePro } from "src/hooks/useEntitlement";
+import { InlineUpgradeToPro, UpgradeToProButton } from "../../UpgradeModal";
+import { Modal } from "components/Modal";
+import { Input } from "components/Input";
+import { deletePublication } from "./deletePublication";
+import { useRouter } from "next/navigation";
+import {
+ isOAuthSessionError,
+ OAuthErrorMessage,
+} from "components/OAuthError";
+
+type SettingsView = "all" | "theme";
+
+export function SettingsContent(props: { showPageBackground: boolean }) {
+ let { data } = usePublicationData();
+ let { publication: pubData } = data || {};
+ let isPro = useIsPro();
+ let canSeePro = useCanSeePro();
+ let record = useNormalizedPublicationRecord();
+ let [loading, setLoading] = useState(false);
+ let toast = useToaster();
+
+ let [nameValue, setNameValue] = useState(record?.name || "");
+ let [descriptionValue, setDescriptionValue] = useState(
+ record?.description || "",
+ );
+ let [iconFile, setIconFile] = useState(null);
+ let [iconPreview, setIconPreview] = useState(null);
+
+ let [showInDiscover, setShowInDiscover] = useState(
+ record?.preferences?.showInDiscover === undefined
+ ? true
+ : record.preferences.showInDiscover,
+ );
+
+ // --- Post Settings state ---
+ let [showComments, setShowComments] = useState(
+ record?.preferences?.showComments === undefined
+ ? true
+ : record.preferences.showComments,
+ );
+ let [showMentions, setShowMentions] = useState(
+ record?.preferences?.showMentions === undefined
+ ? true
+ : record.preferences.showMentions,
+ );
+ let [showRecommends, setShowRecommends] = useState(
+ record?.preferences?.showRecommends === undefined
+ ? true
+ : record.preferences.showRecommends,
+ );
+ let [showPrevNext, setShowPrevNext] = useState(
+ record?.preferences?.showPrevNext === undefined
+ ? true
+ : record.preferences.showPrevNext,
+ );
+
+ // Sync from server data
+ useEffect(() => {
+ if (!pubData || !pubData.record || !record) return;
+ setNameValue(record.name);
+ setDescriptionValue(record.description || "");
+ if (record.icon)
+ setIconPreview(
+ `/api/atproto_images?did=${pubData.identity_did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]}`,
+ );
+ }, [pubData, record]);
+
+ let hasUnsavedChanges = useMemo(() => {
+ if (!record) return false;
+ if (nameValue !== (record.name || "")) return true;
+ if (descriptionValue !== (record.description || "")) return true;
+ if (iconFile !== null) return true;
+
+ let savedShowInDiscover =
+ record.preferences?.showInDiscover === undefined
+ ? true
+ : record.preferences.showInDiscover;
+ if (showInDiscover !== savedShowInDiscover) return true;
+
+ let savedShowComments =
+ record.preferences?.showComments === undefined
+ ? true
+ : record.preferences.showComments;
+ if (showComments !== savedShowComments) return true;
+
+ let savedShowMentions =
+ record.preferences?.showMentions === undefined
+ ? true
+ : record.preferences.showMentions;
+ if (showMentions !== savedShowMentions) return true;
+
+ let savedShowRecommends =
+ record.preferences?.showRecommends === undefined
+ ? true
+ : record.preferences.showRecommends;
+ if (showRecommends !== savedShowRecommends) return true;
+
+ let savedShowPrevNext =
+ record.preferences?.showPrevNext === undefined
+ ? true
+ : record.preferences.showPrevNext;
+ if (showPrevNext !== savedShowPrevNext) return true;
+
+ return false;
+ }, [
+ record,
+ nameValue,
+ descriptionValue,
+ iconFile,
+ showInDiscover,
+ showComments,
+ showMentions,
+ showRecommends,
+ showPrevNext,
+ ]);
+
+ return (
+
+ );
+}
+
+function SettingsFooter(props: { loading: boolean }) {
+ let [distanceFromBottom, setDistanceFromBottom] = useState(Infinity);
+
+ useEffect(() => {
+ const scrollContainer = document.getElementById("home-content");
+ if (!scrollContainer) return;
+
+ const handleScroll = () => {
+ const dist =
+ scrollContainer.scrollHeight -
+ scrollContainer.scrollTop -
+ scrollContainer.clientHeight;
+ setDistanceFromBottom(dist);
+ };
+
+ handleScroll();
+ scrollContainer.addEventListener("scroll", handleScroll);
+ return () => scrollContainer.removeEventListener("scroll", handleScroll);
+ }, []);
+
+ const threshold = 100;
+ // ratio: 1 = far from bottom (full margin), 0 = at bottom (no margin)
+ const ratio = Math.min(distanceFromBottom / threshold, 1);
+ const mx = ratio * 8; // 8px = mx-2
+
+ return (
+
+
+ You have unsaved updates!
+
+ {props.loading ? : "Update Pub"}
+
+
+
+ );
+}
+
+export const DashboardContainer = (props: {
+ children: React.ReactNode;
+ className?: string;
+ section?: string;
+}) => {
+ let cardBorderHidden = useCardBorderHidden();
+ return (
+
+ {props.section && (
+ <>
+ {props.section}
+
+ >
+ )}
+ {props.children}
+
+ );
+};
+
+let pluralize = (n: number, word: string) =>
+ `${n} ${word}${n === 1 ? "" : "s"}`;
+
+const DeletePublication = () => {
+ let [value, setValue] = useState("");
+ let [deleting, setDeleting] = useState(false);
+ let record = useNormalizedPublicationRecord();
+ let { data: pub } = usePublicationData();
+ let postCount = pub?.documents?.length ?? 0;
+ let draftCount = pub?.drafts?.length ?? 0;
+ let subCount = pub?.publication?.publication_subscriptions?.length ?? 0;
+ let toaster = useToaster();
+ let router = useRouter();
+ let pubUri = pub?.publication?.uri;
+
+ let onDelete = async () => {
+ if (!pubUri || record?.name !== value || deleting) return;
+ setDeleting(true);
+ let result = await deletePublication(pubUri);
+ if (!result.success) {
+ setDeleting(false);
+ toaster({
+ type: "error",
+ content: isOAuthSessionError(result.error) ? (
+
+ ) : typeof result.error === "string" ? (
+ result.error
+ ) : (
+ "Failed to delete publication"
+ ),
+ });
+ return;
+ }
+ toaster({
+ type: "success",
+ content: `${record?.name ?? "Publication"} deleted`,
+ });
+ router.push("/home");
+ };
+
+ return (
+ Delete Publication }
+ title="Are you sure?"
+ >
+
+
+ This will permanently delete:
+
+ - This publication and its settings
+ -
+ {pluralize(postCount, "published post")}
+ {postCount > 0 ? " (removed from your PDS)" : ""}
+
+ - {pluralize(draftCount, "draft")}
+ - All associated records on your PDS
+
+ {subCount > 0 && (
+
+ {pluralize(subCount, "subscriber")} will lose access.
+
+ )}
+
+ This cannot be undone.
+
+
+
+ Enter the name of this publication to confirm
+
+
+ setValue(e.currentTarget.value)}
+ />
+
+ {deleting ? : "Delete Publication"}
+
+
+
+ );
+};
diff --git a/app/lish/[did]/[publication]/dashboard/settings/ThemeSettings.tsx b/app/lish/[did]/[publication]/dashboard/settings/ThemeSettings.tsx
new file mode 100644
index 00000000..37e90d52
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/ThemeSettings.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { GoToArrow } from "components/Icons/GoToArrow";
+import { SpeedyLink } from "components/SpeedyLink";
+import { useParams } from "next/navigation";
+
+export function ThemeSettings() {
+ let params = useParams<{ did: string; publication: string }>();
+ let href = `/lish/${params.did}/${params.publication}/theme-settings`;
+
+ return (
+ <>
+
+ Customize Theme
+
+ >
+ );
+}
diff --git a/app/lish/[did]/[publication]/dashboard/settings/deletePublication.ts b/app/lish/[did]/[publication]/dashboard/settings/deletePublication.ts
new file mode 100644
index 00000000..2c4e2467
--- /dev/null
+++ b/app/lish/[did]/[publication]/dashboard/settings/deletePublication.ts
@@ -0,0 +1,124 @@
+"use server";
+
+import { AtpBaseClient } from "lexicons/api";
+import { getIdentityData } from "actions/getIdentityData";
+import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth";
+import { AtUri } from "@atproto/syntax";
+import { supabaseServerClient } from "supabase/serverClient";
+import { drizzle } from "drizzle-orm/node-postgres";
+import {
+ entities,
+ permission_tokens,
+ permission_token_rights,
+} from "drizzle/schema";
+import { eq, inArray } from "drizzle-orm";
+import { pool } from "supabase/pool";
+import { revalidatePath } from "next/cache";
+
+export async function deletePublication(
+ publication_uri: string,
+): Promise<
+ { success: true } | { success: false; error: string | OAuthSessionError }
+> {
+ let identity = await getIdentityData();
+ if (!identity || !identity.atp_did) {
+ return { success: false, error: "Not authenticated" };
+ }
+
+ let pubUri = new AtUri(publication_uri);
+ if (pubUri.host !== identity.atp_did) {
+ return { success: false, error: "Not authorized" };
+ }
+
+ const sessionResult = await restoreOAuthSession(identity.atp_did);
+ if (!sessionResult.ok) {
+ return { success: false, error: sessionResult.error };
+ }
+ let credentialSession = sessionResult.value;
+ let agent = new AtpBaseClient(
+ credentialSession.fetchHandler.bind(credentialSession),
+ );
+
+ // Collect these BEFORE deleting the publication row — cascading deletes would remove the join rows.
+ let [docs, drafts] = await Promise.all([
+ supabaseServerClient
+ .from("documents_in_publications")
+ .select("document")
+ .eq("publication", publication_uri),
+ supabaseServerClient
+ .from("leaflets_in_publications")
+ .select("leaflet")
+ .eq("publication", publication_uri),
+ ]);
+ let documentUris = Array.from(
+ new Set([...(docs.data ?? []).map((r) => r.document)]),
+ );
+ let draftTokenIds = (drafts.data ?? []).map((r) => r.leaflet);
+
+ let pdsDeletes = Promise.all([
+ ...documentUris.flatMap((docUri) => {
+ let u = new AtUri(docUri);
+ if (u.host !== credentialSession.did) return [];
+ return [
+ agent.pub.leaflet.document
+ .delete({ repo: credentialSession.did, rkey: u.rkey })
+ .catch(() => {}),
+ agent.site.standard.document
+ .delete({ repo: credentialSession.did, rkey: u.rkey })
+ .catch(() => {}),
+ ];
+ }),
+ agent.pub.leaflet.publication
+ .delete({ repo: credentialSession.did, rkey: pubUri.rkey })
+ .catch(() => {}),
+ agent.site.standard.publication
+ .delete({ repo: credentialSession.did, rkey: pubUri.rkey })
+ .catch(() => {}),
+ ]);
+
+ let draftDeletes = async () => {
+ if (draftTokenIds.length === 0) return;
+ const client = await pool.connect();
+ try {
+ const db = drizzle(client);
+ await db.transaction(async (tx) => {
+ let tokens = await tx
+ .select()
+ .from(permission_tokens)
+ .leftJoin(
+ permission_token_rights,
+ eq(permission_tokens.id, permission_token_rights.token),
+ )
+ .where(inArray(permission_tokens.id, draftTokenIds));
+ let entitySets = tokens
+ .map((t) => t.permission_token_rights?.entity_set)
+ .filter((s): s is string => !!s);
+ if (entitySets.length > 0) {
+ await tx.delete(entities).where(inArray(entities.set, entitySets));
+ }
+ await tx
+ .delete(permission_tokens)
+ .where(inArray(permission_tokens.id, draftTokenIds));
+ });
+ } finally {
+ client.release();
+ }
+ };
+
+ await Promise.all([pdsDeletes, draftDeletes()]);
+
+ // Delete document rows before publication rows — publication cascade would leave orphaned docs.
+ if (documentUris.length > 0) {
+ await supabaseServerClient
+ .from("documents")
+ .delete()
+ .in("uri", documentUris);
+ }
+ await supabaseServerClient
+ .from("publications")
+ .delete()
+ .eq("uri", publication_uri);
+
+ revalidatePath("/lish/[did]/[publication]", "layout");
+ return { success: true };
+}
diff --git a/app/lish/[did]/[publication]/page.tsx b/app/lish/[did]/[publication]/page.tsx
index c4fce3f1..d3901586 100644
--- a/app/lish/[did]/[publication]/page.tsx
+++ b/app/lish/[did]/[publication]/page.tsx
@@ -1,40 +1,28 @@
import { supabaseServerClient } from "supabase/serverClient";
-import { AtUri } from "@atproto/syntax";
import {
getPublicationURL,
getDocumentURL,
} from "app/lish/createPub/getPublicationURL";
import { BskyAgent } from "@atproto/api";
import { publicationNameOrUriFilter } from "src/utils/uriHelpers";
-import { SubscribeWithBluesky } from "app/lish/Subscribe";
import React from "react";
+import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
+import { normalizePublicationRecord } from "src/utils/normalizeRecords";
+import { PublicationContent } from "./PublicationContent";
import {
- PublicationBackgroundProvider,
PublicationThemeProvider,
+ PublicationBackgroundProvider,
} from "components/ThemeManager/PublicationThemeProvider";
-import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
-import { SpeedyLink } from "components/SpeedyLink";
-import { InteractionPreview } from "components/InteractionsPreview";
-import { LocalizedDate } from "./LocalizedDate";
-import { PublicationHomeLayout } from "./PublicationHomeLayout";
-import { PublicationAuthor } from "./PublicationAuthor";
-import { Separator } from "components/Layout";
-import {
- normalizePublicationRecord,
- normalizeDocumentRecord,
-} from "src/utils/normalizeRecords";
-import { getFirstParagraph } from "src/utils/getFirstParagraph";
-import { FontLoader } from "components/FontLoader";
export default async function Publication(props: {
params: Promise<{ publication: string; did: string }>;
}) {
let params = await props.params;
- let did = decodeURIComponent(params.did);
+ const did = decodeURIComponent(params.did);
if (!did) return ;
- let agent = new BskyAgent({ service: "https://public.api.bsky.app" });
- let publication_name = decodeURIComponent(params.publication);
- let [{ data: publications }, { data: profile }] = await Promise.all([
+ const agent = new BskyAgent({ service: "https://public.api.bsky.app" });
+ const publication_name = decodeURIComponent(params.publication);
+ const [{ data: publications }, { data: profile }] = await Promise.all([
supabaseServerClient
.from("publications")
.select(
@@ -54,162 +42,32 @@ export default async function Publication(props: {
.limit(1),
agent.getProfile({ actor: did }),
]);
- let publication = publications?.[0];
+ const publication = publications?.[0];
const record = normalizePublicationRecord(publication?.record);
- let showPageBackground = record?.theme?.showPageBackground;
+ const showPageBackground = record?.theme?.showPageBackground;
if (!publication) return ;
try {
return (
- <>
-
-
+
-
-
-
- {record?.icon && (
-
- )}
-
- {publication.name}
-
-
- {record?.description}{" "}
-
- {profile && (
-
- )}
-
-
-
-
-
- {publication.documents_in_publications
- .filter((d) => !!d?.documents)
- .sort((a, b) => {
- const aRecord = normalizeDocumentRecord(a.documents?.data);
- const bRecord = normalizeDocumentRecord(b.documents?.data);
- const aDate = aRecord?.publishedAt
- ? new Date(aRecord.publishedAt)
- : new Date(0);
- const bDate = bRecord?.publishedAt
- ? new Date(bRecord.publishedAt)
- : new Date(0);
- return bDate.getTime() - aDate.getTime(); // Sort by most recent first
- })
- .map((doc) => {
- if (!doc.documents) return null;
- const doc_record = normalizeDocumentRecord(
- doc.documents.data,
- );
- if (!doc_record) return null;
- let uri = new AtUri(doc.documents.uri);
- let quotes =
- doc.documents.document_mentions_in_bsky[0].count || 0;
- let comments =
- record?.preferences?.showComments === false
- ? 0
- : doc.documents.comments_on_documents[0].count || 0;
- let recommends =
- doc.documents.recommends_on_documents?.[0]?.count || 0;
- let tags = doc_record.tags || [];
-
- const docUrl = getDocumentURL(
- doc_record,
- doc.documents.uri,
- publication,
- );
- return (
-
-
-
- {doc_record.title && (
-
- {doc_record.title}
-
- )}
-
- {doc_record.description ||
- getFirstParagraph(doc_record)}
-
-
-
-
-
- {doc_record.publishedAt && (
-
- )}{" "}
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
- >
+
+
+
);
} catch (e) {
console.log(e);
diff --git a/app/lish/[did]/[publication]/theme-settings/PostPreview.tsx b/app/lish/[did]/[publication]/theme-settings/PostPreview.tsx
new file mode 100644
index 00000000..e880d6f1
--- /dev/null
+++ b/app/lish/[did]/[publication]/theme-settings/PostPreview.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { LinearDocumentPage } from "app/lish/[did]/[publication]/[rkey]/LinearDocumentPage";
+import { LeafletContentProvider } from "contexts/LeafletContentContext";
+import {
+ DocumentProvider,
+ type PublicationContext,
+} from "contexts/DocumentContext";
+import { useIdentityData } from "components/IdentityProvider";
+import type { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
+import type { PubLeafletPagesLinearDocument } from "lexicons/api";
+import type { PostPageData } from "app/lish/[did]/[publication]/[rkey]/getPostPageData";
+import type { DocumentContextValue } from "contexts/DocumentContext";
+import {
+ usePublicationData,
+ useNormalizedPublicationRecord,
+} from "../dashboard/PublicationSWRProvider";
+import { fakeBlocks, fakePage } from "./postPreviewFakeBlocks";
+
+const FAKE_DID = "did:plc:fake-preview-user";
+const FAKE_DOC_URI =
+ "at://did:plc:fake-preview-user/site.standard.document/preview";
+
+const fakeNormalizedDocument = {
+ $type: "site.standard.document" as const,
+ title: "Building your Dream Theme!",
+ description: "A short description of this preview post.",
+ publishedAt: new Date().toISOString(),
+ site: "at://did:plc:fake-preview-user/site.standard.publication/preview",
+ tags: ["preview", "theme"],
+};
+
+function makeFakeDocument(
+ publication?: {
+ uri: string;
+ name: string;
+ identity_did: string;
+ record: unknown;
+ } | null,
+): NonNullable {
+ return {
+ data: {},
+ uri: FAKE_DOC_URI,
+ normalizedDocument: fakeNormalizedDocument,
+ normalizedPublication: null,
+ quotesAndMentions: [],
+ theme: null,
+ prevNext: undefined,
+ publication: publication || null,
+ comments: [],
+ comments_on_documents: [],
+ mentions: [],
+ document_mentions_in_bsky: [],
+ leaflets_in_publications: [],
+ leafletId: null,
+ recommendsCount: 0,
+ documents_in_publications: publication
+ ? [{ publications: publication }]
+ : [],
+ recommends_on_documents: [],
+ } as unknown as NonNullable;
+}
+
+export function PostPreview(props: {
+ showPageBackground: boolean;
+ pageWidth: number;
+}) {
+ let { identity } = useIdentityData();
+ let { data } = usePublicationData();
+ let { publication } = data || {};
+ let record = useNormalizedPublicationRecord();
+ let preferences = record?.preferences;
+ let profileRecord = identity?.bsky_profiles
+ ?.record as unknown as ProfileViewDetailed;
+
+ let profile = profileRecord ?? {
+ did: FAKE_DID,
+ handle: "preview.bsky.social",
+ displayName: "Preview Author",
+ };
+
+ let pubInfo: PublicationContext = publication
+ ? {
+ uri: publication.uri,
+ name: publication.name,
+ identity_did: publication.identity_did,
+ record: publication.record as NonNullable["record"],
+ publication_subscriptions: (
+ publication.publication_subscriptions || []
+ ).map((s) => ({
+ created_at: s.created_at,
+ identity: s.identity,
+ publication: s.publication,
+ record: s.record,
+ uri: s.uri,
+ })),
+ }
+ : null;
+
+ let fakeDocument = makeFakeDocument(pubInfo);
+
+ let fakeDocumentContextValue: DocumentContextValue = {
+ uri: FAKE_DOC_URI,
+ normalizedDocument: fakeNormalizedDocument,
+ normalizedPublication: null,
+ theme: undefined,
+ prevNext: undefined,
+ quotesAndMentions: [],
+ publication: pubInfo,
+ comments: [],
+ mentions: [],
+ leafletId: null,
+ recommendsCount: 0,
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/lish/[did]/[publication]/theme-settings/PubPreview.tsx b/app/lish/[did]/[publication]/theme-settings/PubPreview.tsx
new file mode 100644
index 00000000..27887ef4
--- /dev/null
+++ b/app/lish/[did]/[publication]/theme-settings/PubPreview.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import {
+ usePublicationData,
+ useNormalizedPublicationRecord,
+} from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider";
+import { useIdentityData } from "components/IdentityProvider";
+import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
+import { PublicationContent } from "../PublicationContent";
+import { LocalizedDate } from "../LocalizedDate";
+
+export function PubPreview(props: {
+ showPageBackground: boolean;
+ pageWidth: number;
+}) {
+ let { data } = usePublicationData();
+ let { publication } = data || {};
+ let { identity } = useIdentityData();
+ let record = useNormalizedPublicationRecord();
+
+ let profileRecord = identity?.bsky_profiles
+ ?.record as unknown as ProfileViewDetailed;
+
+ let did = publication?.identity_did || "";
+
+ let profile = identity?.bsky_profiles
+ ? {
+ did: identity.bsky_profiles.did,
+ displayName: profileRecord?.displayName,
+ handle: identity.bsky_profiles.handle || "",
+ }
+ : undefined;
+
+ if (!publication) return null;
+
+ const hasPosts = publication.documents_in_publications.some(
+ (d) => !!d?.documents,
+ );
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(today.getDate() - 1);
+ const dayBefore = new Date(today);
+ dayBefore.setDate(today.getDate() - 2);
+
+ const dateOptions: Intl.DateTimeFormatOptions = {
+ year: "numeric",
+ month: "long",
+ day: "2-digit",
+ };
+
+ const fakePosts = !hasPosts
+ ? undefined
+ : [
+ {
+ title: "Your Personal Antheme",
+ description:
+ "Welcome to the Publication Theme Setter. This is how posts will appear in your publication",
+ date: (
+
+ ),
+ },
+ {
+ title: "The Theme of the Crop",
+ description:
+ "This is the place to make your publication look and feel like home. It looks great!",
+ date: (
+
+ ),
+ },
+ {
+ title: "Reams and Reams of Colorful Themes!",
+ description:
+ "So happy to have you. There's so much cool stuff happening here, including this publication :)",
+ date: (
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx b/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx
new file mode 100644
index 00000000..51a284a7
--- /dev/null
+++ b/app/lish/[did]/[publication]/theme-settings/ThemeSettingsContent.tsx
@@ -0,0 +1,277 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { useParams, useRouter } from "next/navigation";
+import {
+ BaseThemeProvider,
+ CardBorderHiddenContext,
+} from "components/ThemeManager/ThemeProvider";
+import { ButtonPrimary, ButtonSecondary } from "components/Buttons";
+import { DotLoader } from "components/utils/DotLoader";
+import { ToggleGroup } from "components/ToggleGroup";
+import { PaintSmall } from "components/Icons/PaintSmall";
+import { Popover } from "components/Popover";
+import {
+ usePubThemeEditorState,
+ PubThemePickerPanel,
+} from "components/ThemeManager/PubThemeSetter";
+import { PubPreview } from "./PubPreview";
+import { PostPreview } from "./PostPreview";
+import { PublicationBackgroundProvider } from "components/ThemeManager/PublicationThemeProvider";
+import {
+ usePublicationData,
+ useNormalizedPublicationRecord,
+} from "../dashboard/PublicationSWRProvider";
+import { Separator } from "components/Layout";
+import { GoToArrow } from "components/Icons/GoToArrow";
+import Link from "next/link";
+
+export function ThemeSettingsContent() {
+ let toolbarRef = useRef(null);
+ let [previewMode, setPreviewMode] = useState<"post" | "pub">("post");
+ let params = useParams<{ did: string; publication: string }>();
+ let { data } = usePublicationData();
+ let { publication } = data || {};
+ let record = useNormalizedPublicationRecord();
+ let state = usePubThemeEditorState();
+ let {
+ localPubTheme,
+ headingFont,
+ bodyFont,
+ image,
+ pageWidth,
+ pubBGImage,
+ leafletBGRepeat,
+ showPageBackground,
+ changes,
+ } = state;
+
+ let settingsHref = `/lish/${params.did}/${params.publication}/dashboard?tab=Settings`;
+
+ let hasUnsavedChanges =
+ changes ||
+ headingFont !== record?.theme?.headingFont ||
+ bodyFont !== record?.theme?.bodyFont ||
+ pageWidth !== (record?.theme?.pageWidth || 624) ||
+ showPageBackground !== !!record?.theme?.showPageBackground;
+
+ return (
+
+
+
+ {/* Theme Setter Panel */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Full-page Preview */}
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ {previewMode === "pub" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ );
+}
+
+const BackToPubButton = (props: {
+ hasUnsavedChanges: boolean;
+ settingsHref: string;
+ localPubTheme: ReturnType["localPubTheme"];
+ headingFont: ReturnType["headingFont"];
+ bodyFont: ReturnType["bodyFont"];
+ image: ReturnType["image"];
+ pageWidth: ReturnType["pageWidth"];
+}) => {
+ let router = useRouter();
+ let [open, setOpen] = useState(false);
+ if (props.hasUnsavedChanges)
+ return (
+
+
+ Back To Settings
+
+ }
+ >
+
+
+ Discard unsaved changes?
+
+ You have unsaved changes to your theme. Leaving the page will lose
+ your edits!
+
+
+ router.push(props.settingsHref)}
+ >
+ Discard and Leave
+
+
+
+
+
+
+
+ );
+ else
+ return (
+
+
+
+ Back To Settings
+
+
+ );
+};
+
+const PubThemePopover = ({
+ state,
+ toolbarRef,
+}: {
+ state: ReturnType;
+ toolbarRef: React.RefObject;
+}) => {
+ let {
+ localPubTheme,
+ headingFont,
+ bodyFont,
+ image,
+ pageWidth,
+ submitTheme,
+ toaster,
+ } = state;
+ let [loading, setLoading] = useState(false);
+
+ return (
+
+
+
+ }
+ asChild
+ >
+
+
+ {/* Toggle + Save Header */}
+
+
+ {
+ let result = await submitTheme(setLoading);
+ if (result?.success) {
+ toaster({
+ content: "Theme saved!",
+ type: "success",
+ });
+ }
+ }}
+ >
+ {loading ? : "Save Changes"}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/app/lish/[did]/[publication]/theme-settings/page.tsx b/app/lish/[did]/[publication]/theme-settings/page.tsx
new file mode 100644
index 00000000..caffe8fd
--- /dev/null
+++ b/app/lish/[did]/[publication]/theme-settings/page.tsx
@@ -0,0 +1,62 @@
+import { supabaseServerClient } from "supabase/serverClient";
+import { getIdentityData } from "actions/getIdentityData";
+import { get_publication_data } from "app/api/rpc/[command]/get_publication_data";
+import { PublicationSWRDataProvider } from "../dashboard/PublicationSWRProvider";
+import { AtUri } from "@atproto/syntax";
+import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
+import { normalizePublicationRecord } from "src/utils/normalizeRecords";
+import { ThemeSettingsContent } from "./ThemeSettingsContent";
+
+export default async function ThemeSettingsPage(props: {
+ params: Promise<{ publication: string; did: string }>;
+}) {
+ let params = await props.params;
+ let identity = await getIdentityData();
+ if (!identity || !identity.atp_did)
+ return (
+
+ Looks like you're not logged in.
+
+ If the issue persists please{" "}
+ send us a note.
+
+
+ );
+ let did = decodeURIComponent(params.did);
+ if (!did) return ;
+ let { result: publication_data } = await get_publication_data.handler(
+ {
+ did,
+ publication_name: decodeURIComponent(params.publication),
+ },
+ { supabase: supabaseServerClient },
+ );
+ let { publication } = publication_data;
+ const record = normalizePublicationRecord(publication?.record);
+
+ if (!publication || identity.atp_did !== publication.identity_did || !record)
+ return ;
+ let uri = new AtUri(publication.uri);
+
+ return (
+
+
+
+ );
+}
+
+const ThemeNotFound = () => {
+ return (
+
+ Sorry, we can't find this publication!
+
+ This may be a glitch on our end. If the issue persists please{" "}
+ send us a note.
+
+
+ );
+};
diff --git a/app/lish/[did]/[publication]/theme-settings/postPreviewFakeBlocks.ts b/app/lish/[did]/[publication]/theme-settings/postPreviewFakeBlocks.ts
new file mode 100644
index 00000000..77353f38
--- /dev/null
+++ b/app/lish/[did]/[publication]/theme-settings/postPreviewFakeBlocks.ts
@@ -0,0 +1,353 @@
+import type { PubLeafletPagesLinearDocument } from "lexicons/api";
+
+export const fakeBlocks: PubLeafletPagesLinearDocument.Block[] = [
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "Welcome to Leaflet, intrepid writer!",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "This is a place to write, blog, journal, and above all, express oneself. As such we take theming very seriously. Read on to discover how to make your wildest themes come true!",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: 'And now, for a horizontal rule (also known as a "divider")!',
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.horizontalRule",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 2,
+ plaintext: "TLDR",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "We have some great presets available! Go ahead and apply one of those, and mess with the accent color to make it yours. Just keep in mind",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.unorderedList",
+ children: [
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "Go for a nice bright accent ",
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "Make sure the text on accent is still legible. White or black if you're not sure!",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.horizontalRule",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 2,
+ plaintext: "Your Text",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 3,
+ plaintext: "Text",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "This is your default text color. ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "We also mix it with your background color to make lighter text or border colors!",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "No need to think too hard, black or white is good! Just make sure it shows up strong against your background color. ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 3,
+ plaintext: "Accent Colors ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ facets: [
+ {
+ index: {
+ byteEnd: 32,
+ byteStart: 20,
+ },
+ features: [
+ {
+ uri: "https://leaflet.pub/about",
+ $type: "pub.leaflet.richtext.facet#link",
+ },
+ ],
+ },
+ ],
+ plaintext:
+ "We use this in your inline links, and in certain block types like...",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ url: "https://www.leaflet.pub/about",
+ text: "Buttons!",
+ $type: "pub.leaflet.blocks.button",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "Pick something... ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.unorderedList",
+ children: [
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "nice and bright",
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "with legible text on accent (white usually works, but sometimes black shows up better) ",
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "that shows off your personality!",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.horizontalRule",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 2,
+ plaintext: "Your Background",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 3,
+ plaintext: "Background",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "This is your background color. It can also be an image!",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "If you decide to go for a solid background color...",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.unorderedList",
+ children: [
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "pick one that isn't too vibrant",
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "go either dark or light, not in the middle",
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "try a very dark or very light version of your accent color",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "If you go for an image, it's easy to overwhelm a reader with too much, so pick one that's... ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.unorderedList",
+ children: [
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ 'not distracting. ie, not colorful or busy. You want something that could be described as "kinda boring". It\'ll still give a lot of personality once you apply it!',
+ },
+ },
+ {
+ $type: "pub.leaflet.blocks.unorderedList#listItem",
+ content: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "Gradients are a classy classic",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 3,
+ plaintext: "Page or Container",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "You can choose to have a page background or not. ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "Page background puts a box around your writing. It's especially useful if you have a background image making your text harder to read. It's also another place to inject a color to give your writing some zuzsh.",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "No page background looks clean and minimal. It's sup to you!",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.horizontalRule",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.header",
+ level: 2,
+ plaintext: "Stuck?",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext:
+ "If you're really stuck, try one of our preset themes! Change just the accent color to give it a more personal flair. ",
+ },
+ },
+ {
+ $type: "pub.leaflet.pages.linearDocument#block",
+ block: {
+ $type: "pub.leaflet.blocks.text",
+ plaintext: "Good luck!",
+ },
+ },
+];
+
+export const fakePage: PubLeafletPagesLinearDocument.Main = {
+ $type: "pub.leaflet.pages.linearDocument",
+ id: "preview-page",
+ blocks: fakeBlocks,
+};
diff --git a/app/lish/createPub/CreatePubForm.tsx b/app/lish/createPub/CreatePubForm.tsx
index c6138dac..a2f98267 100644
--- a/app/lish/createPub/CreatePubForm.tsx
+++ b/app/lish/createPub/CreatePubForm.tsx
@@ -149,13 +149,13 @@ export const CreatePubForm = () => {
onChange={(e) => setShowInDiscover(e.target.checked)}
>
- Show In Discover
+ Make Public
Your posts will appear in{" "}
Leaflet Reader
-
- . You can change this at any time!
+ {" "}
+ and show up in search and tags. You can change this at any time!
diff --git a/app/lish/createPub/UpdatePubForm.tsx b/app/lish/createPub/UpdatePubForm.tsx
index 28464c4b..32bdc331 100644
--- a/app/lish/createPub/UpdatePubForm.tsx
+++ b/app/lish/createPub/UpdatePubForm.tsx
@@ -10,7 +10,6 @@ import {
import { mutate } from "swr";
import { AddTiny } from "components/Icons/AddTiny";
import { useToaster } from "components/Toast";
-import { PubSettingsHeader } from "../[did]/[publication]/dashboard/settings/PublicationSettings";
import { Toggle } from "components/Toggle";
export const EditPubForm = (props: {
@@ -85,13 +84,7 @@ export const EditPubForm = (props: {
mutate("publication-data");
}}
>
-
- General Settings
-
+
@@ -165,13 +158,13 @@ export const EditPubForm = (props: {
onToggle={() => setShowInDiscover(!showInDiscover)}
>
- Show In Discover
-
+
Make Public
+
Your posts will appear in{" "}
Leaflet Reader
-
- . You can change this at any time!
+ {" "}
+ and show up in search and tags. You can change this at any time!
@@ -179,4 +172,3 @@ export const EditPubForm = (props: {
);
};
-
diff --git a/app/lish/createPub/page.tsx b/app/lish/createPub/page.tsx
index bb33a11a..f4d4d510 100644
--- a/app/lish/createPub/page.tsx
+++ b/app/lish/createPub/page.tsx
@@ -9,7 +9,7 @@ export default async function CreatePub() {
return (
-
+
{
{" "}
-
+
+
+
>
)}
diff --git a/components/Domains/PublicationDomains.tsx b/components/Domains/PublicationDomains.tsx
index 802d45d5..3cfa3881 100644
--- a/components/Domains/PublicationDomains.tsx
+++ b/components/Domains/PublicationDomains.tsx
@@ -17,7 +17,7 @@ import {
assignDomainToPublication,
removeDomainAssignment,
} from "actions/domains";
-import { PubSettingsHeader } from "app/lish/[did]/[publication]/dashboard/settings/PublicationSettings";
+
import { AddDomainForm } from "./AddDomainForm";
import { DomainSettingsView } from "./DomainSettingsView";
import { PinTiny } from "components/Icons/PinTiny";
@@ -44,9 +44,7 @@ export function PublicationDomains(props: {
return (
-
- Domains
-
+ Domains
This Publication's Domains
DEFAULT
diff --git a/components/EmptyState.tsx b/components/EmptyState.tsx
new file mode 100644
index 00000000..c85b036e
--- /dev/null
+++ b/components/EmptyState.tsx
@@ -0,0 +1,30 @@
+export function EmptyState({
+ title,
+ description,
+ className,
+ container = "frosted",
+ children,
+}: {
+ title?: string;
+ description?: string;
+ className?: string;
+ container?: "frosted" | "opaque" | "none";
+ children?: React.ReactNode;
+}) {
+ const containerClass =
+ container === "frosted"
+ ? "frosted-container"
+ : container === "opaque"
+ ? "opaque-container"
+ : "";
+
+ return (
+
+ {title && {title}}
+ {description && {description}}
+ {children}
+
+ );
+}
diff --git a/components/Icons/PasteTiny.tsx b/components/Icons/PasteTiny.tsx
new file mode 100644
index 00000000..74c1284b
--- /dev/null
+++ b/components/Icons/PasteTiny.tsx
@@ -0,0 +1,19 @@
+import { Props } from "./Props";
+
+export const PasteTiny = (props: Props) => {
+ return (
+
+ );
+};
diff --git a/components/Icons/SettingsTiny.tsx b/components/Icons/SettingsTiny.tsx
new file mode 100644
index 00000000..ee4f5d4b
--- /dev/null
+++ b/components/Icons/SettingsTiny.tsx
@@ -0,0 +1,21 @@
+import { Props } from "./Props";
+
+export const SettingsTiny = (props: Props) => {
+ return (
+
+ );
+};
diff --git a/components/Modal.tsx b/components/Modal.tsx
index 625150f8..ff108f8a 100644
--- a/components/Modal.tsx
+++ b/components/Modal.tsx
@@ -41,7 +41,7 @@ export const Modal = ({
${className}`}
>
{title ? (
-
+
{title}
) : (
diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx
index 0e61a9f2..5e5b8e02 100644
--- a/components/PageHeader.tsx
+++ b/components/PageHeader.tsx
@@ -21,9 +21,7 @@ export const Header = (props: { children: React.ReactNode }) => {
}
}, []);
- let headerBGColor = !cardBorderHidden
- ? "var(--bg-leaflet)"
- : "var(--bg-page)";
+ let headerBGColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)";
return (
(props: {
@@ -186,7 +187,7 @@ export function DashboardLayout<
@@ -206,6 +207,7 @@ export function DashboardLayout<
setTabWithUrl(t)}
onMouseEnter={() => props.onTabHover?.(t)}
@@ -215,7 +217,7 @@ export function DashboardLayout<
})}
)}
- {props.publication && (
+ {props.publication && controls && (