From ca3b7b51ed4864357be9074ab05360dd362fad52 Mon Sep 17 00:00:00 2001 From: celine Date: Wed, 25 Mar 2026 21:59:22 -0400 Subject: [PATCH] WIP move everything in pub setting popover to tab --- .../[did]/[publication]/dashboard/Actions.tsx | 2 - .../dashboard/PublicationDashboard.tsx | 9 + .../dashboard/settings/DomainsInline.tsx | 371 ++++++++++++++++++ .../dashboard/settings/SettingsTab.tsx | 285 ++++++++++++++ 4 files changed, 665 insertions(+), 2 deletions(-) create mode 100644 app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx create mode 100644 app/lish/[did]/[publication]/dashboard/settings/SettingsTab.tsx 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..23b3829a 100644 --- a/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx +++ b/app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx @@ -14,6 +14,7 @@ import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; import { type NormalizedPublication } from "src/utils/normalizeRecords"; import { PublicationAnalytics } from "./PublicationAnalytics"; import { useCanSeePro } from "src/hooks/useEntitlement"; +import { SettingsTab } from "./settings/SettingsTab"; export default function PublicationDashboard({ publication, @@ -87,6 +88,14 @@ export default function PublicationDashboard({ }, } : {}), + Settings: { + content: ( + + ), + controls: null, + }, }} actions={} currentPage="pub" diff --git a/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx b/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx new file mode 100644 index 00000000..7e7a03f9 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/settings/DomainsInline.tsx @@ -0,0 +1,371 @@ +"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 DomainsInline = () => { + 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 ( +
+

Domains

+
+

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); + 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, + }); + } + + 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/SettingsTab.tsx b/app/lish/[did]/[publication]/dashboard/settings/SettingsTab.tsx new file mode 100644 index 00000000..492a4a41 --- /dev/null +++ b/app/lish/[did]/[publication]/dashboard/settings/SettingsTab.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { Input } from "components/Input"; +import { Toggle } from "components/Toggle"; +import { ButtonPrimary } from "components/Buttons"; +import { DotLoader } from "components/utils/DotLoader"; +import { AddTiny } from "components/Icons/AddTiny"; +import { useToaster } from "components/Toast"; +import { mutate } from "swr"; +import { + usePublicationData, + useNormalizedPublicationRecord, +} from "../PublicationSWRProvider"; +import { updatePublication } from "app/lish/createPub/updatePublication"; +import { DomainsInline } from "./DomainsInline"; +import { PubThemeSetter } from "components/ThemeManager/PubThemeSetter"; + +type SettingsView = "all" | "theme"; + +export function SettingsTab(props: { showPageBackground: boolean }) { + let [view, setView] = useState("all"); + let [themeLoading, setThemeLoading] = useState(false); + + if (view === "theme") { + return ( +
+ setView("all")} + loading={themeLoading} + setLoading={setThemeLoading} + /> +
+ ); + } + + return setView("theme")} />; +} + +function SettingsForm(props: { onOpenTheme: () => void }) { + let { data } = usePublicationData(); + let { publication: pubData } = data || {}; + let record = useNormalizedPublicationRecord(); + let [loading, setLoading] = useState(false); + let toast = useToaster(); + + // --- General Settings state --- + let [nameValue, setNameValue] = useState(record?.name || ""); + let [descriptionValue, setDescriptionValue] = useState( + record?.description || "", + ); + let [iconFile, setIconFile] = useState(null); + let [iconPreview, setIconPreview] = useState(null); + let fileInputRef = useRef(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]); + + return ( +
{ + e.preventDefault(); + if (!pubData) return; + setLoading(true); + await updatePublication({ + uri: pubData.uri, + name: nameValue, + description: descriptionValue, + iconFile: iconFile, + preferences: { + showInDiscover, + showComments, + showMentions, + showPrevNext, + showRecommends, + }, + }); + toast({ type: "success", content: "Settings saved!" }); + setLoading(false); + mutate("publication-data"); + }} + > +
+ {/* ── General Settings ── */} +
+

General Settings

+ +
+

+ Logo (optional) +

+
fileInputRef.current?.click()} + > + {iconPreview ? ( + Logo preview + ) : ( + + )} +
+ { + const file = e.target.files?.[0]; + if (file) { + setIconFile(file); + const reader = new FileReader(); + reader.onload = (ev) => { + setIconPreview(ev.target?.result as string); + }; + reader.readAsDataURL(file); + } + }} + /> +
+ + + + + + setShowInDiscover(!showInDiscover)} + > +
+

Show In Discover

+

+ Your posts will appear in{" "} + + Leaflet Reader + + . You can change this at any time! +

+
+
+
+ +
+ + {/* ── Post Settings ── */} +
+

Post Settings

+ +

Layout

+ setShowPrevNext(!showPrevNext)} + > +
Show Prev/Next Buttons
+
+ +
+ +

Interactions

+
+ setShowComments(!showComments)} + > +
Show Comments
+
+ + setShowMentions(!showMentions)} + > +
+
Show Mentions
+
+ Display a list of Bluesky mentions about your post +
+
+
+ + setShowRecommends(!showRecommends)} + > +
+
Show Recommends
+
+ Allow readers to recommend/like your post +
+
+
+
+
+ +
+ + {/* ── Theme ── */} +
+

Theme and Layout

+ +
+ +
+ + {/* ── Domains ── */} + +
+ + {/* ── Sticky Save Footer ── */} +
+ + {loading ? : "Save Changes"} + +
+
+ ); +} -- 2.51.2