diff --git a/actions/updatePublicationPage.ts b/actions/updatePublicationPage.ts new file mode 100644 index 00000000..72f62b2d --- /dev/null +++ b/actions/updatePublicationPage.ts @@ -0,0 +1,30 @@ +"use server"; +import { getIdentityData } from "actions/getIdentityData"; +import { supabaseServerClient } from "supabase/serverClient"; + +export async function updatePublicationPage(args: { + publication_uri: string; + page_id: number; + title: string; + path: string; +}): Promise<{ success: boolean }> { + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) return { success: false }; + + let { data: publication } = await supabaseServerClient + .from("publications") + .select("uri, identity_did") + .eq("uri", args.publication_uri) + .single(); + if (!publication || publication.identity_did !== identity.atp_did) + return { success: false }; + + let { error } = await supabaseServerClient + .from("publication_pages") + .update({ title: args.title, path: args.path }) + .eq("id", args.page_id) + .eq("publication", args.publication_uri); + if (error) return { success: false }; + + return { success: true }; +} diff --git a/app/(app)/lish/[did]/[publication]/PublicationContent.tsx b/app/(app)/lish/[did]/[publication]/PublicationContent.tsx index 137f8383..6d5a4b14 100644 --- a/app/(app)/lish/[did]/[publication]/PublicationContent.tsx +++ b/app/(app)/lish/[did]/[publication]/PublicationContent.tsx @@ -1,10 +1,6 @@ import React from "react"; import { PublicationHomeLayout } from "./PublicationHomeLayout"; import { PublicationAuthor } from "./PublicationAuthor"; -import { PublicationHeader } from "./PublicationHeader"; -import { PublicationNav } from "./PublicationNav"; -import { PublicationStickyHeader } from "./PublicationStickyHeader"; -import { PublicationFullHeader } from "./PublicationFullHeader"; import { normalizePublicationRecord, normalizeDocumentRecord, @@ -91,56 +87,31 @@ export const PublicationContent = ({ 0 ? ( - - } - > - - - ) : ( - - - ) : undefined - } - subscribeButton={ -
- -
- } - /> -
- ) + iconUrl={record?.icon ? blobRefToSrc(record.icon.ref, did) : undefined} + publicationName={publication.name} + description={record?.description} + navPages={navPages} + publicationUrl={getPublicationURL(publication)} + activePath="/" + author={ + profile ? ( + + ) : undefined + } + subscribeButton={ +
+ +
} > -
- {props.children} -
- - ); -} diff --git a/app/(app)/lish/[did]/[publication]/PublicationHomeLayout.tsx b/app/(app)/lish/[did]/[publication]/PublicationHomeLayout.tsx index 4b052694..2cda91a6 100644 --- a/app/(app)/lish/[did]/[publication]/PublicationHomeLayout.tsx +++ b/app/(app)/lish/[did]/[publication]/PublicationHomeLayout.tsx @@ -1,17 +1,60 @@ "use client"; +import React from "react"; import { usePreserveScroll } from "src/hooks/usePreserveScroll"; +import { PublicationHeader } from "./PublicationHeader"; +import { PublicationNav, type PublicationNavPage } from "./PublicationNav"; +import { PublicationStickyHeader } from "./PublicationStickyHeader"; export function PublicationHomeLayout(props: { uri: string; showPageBackground: boolean; - stickyHeader?: React.ReactNode; + iconUrl?: string; + publicationName: string; + description?: string; + author?: React.ReactNode; + subscribeButton?: React.ReactNode; + navPages: PublicationNavPage[]; + publicationUrl: string; + activePath: string; children: React.ReactNode; }) { let { ref } = usePreserveScroll(props.uri); + let hasNav = props.navPages.length > 0; + + let header = hasNav ? ( + + } + > + + + ) : ( +
+
+ +
+
+ ); + let inner = ( <> - {props.stickyHeader} + {header}
{props.children}
diff --git a/app/(app)/lish/[did]/[publication]/PublicationStickyHeader.tsx b/app/(app)/lish/[did]/[publication]/PublicationStickyHeader.tsx index dd653e86..68e9409d 100644 --- a/app/(app)/lish/[did]/[publication]/PublicationStickyHeader.tsx +++ b/app/(app)/lish/[did]/[publication]/PublicationStickyHeader.tsx @@ -3,13 +3,11 @@ import { useEffect, useRef } from "react"; import { usePathname } from "next/navigation"; export function PublicationStickyHeader(props: { - sticky?: boolean; nav: React.ReactNode; children: React.ReactNode; }) { let ref = useRef(null); let pathname = usePathname(); - let sticky = props.sticky ?? true; useEffect(() => { let el = ref.current; @@ -62,11 +60,7 @@ export function PublicationStickyHeader(props: { return (
0 ? ( - - } - > - - - ) : ( - - - - ) + iconUrl={ + normalizedPublication?.icon + ? blobRefToSrc(normalizedPublication.icon.ref, did) + : undefined } + publicationName={publication.name} + description={normalizedPublication?.description} + navPages={navPages} + publicationUrl={getPublicationURL(publication)} + activePath={page.path} >
{ - mutate(key, props.publication_data); - }, [props.publication_data]); return ( 1} + onUpdated={(updated) => { + mutatePublicationData(mutate, (draft) => { + let p = draft.publication?.publication_pages.find( + (p) => p.id === page.id, + ); + if (p) { + p.title = updated.title; + p.path = updated.path; + } + }); + if (isActive && updated.path !== page.path) + router.push(hrefForPath(updated.path)); + }} onDeleted={() => { mutatePublicationData(mutate, (draft) => { let pages = draft.publication?.publication_pages; @@ -231,6 +244,7 @@ function SortableTab(props: { active: boolean; publicationUri: string | undefined; canDelete: boolean; + onUpdated: (updated: { title: string; path: string }) => void; onDeleted: () => void; }) { let { @@ -242,7 +256,11 @@ function SortableTab(props: { transition, isDragging, } = useSortable({ id: props.page.id }); - let [confirmOpen, setConfirmOpen] = useState(false); + let [popoverOpen, setPopoverOpen] = useState(false); + let [mode, setMode] = useState<"edit" | "confirm">("edit"); + let [name, setName] = useState(props.page.title); + let [path, setPath] = useState(props.page.path ?? "/"); + let [saving, setSaving] = useState(false); let [deleting, setDeleting] = useState(false); let style = { @@ -251,6 +269,35 @@ function SortableTab(props: { zIndex: isDragging ? 1 : undefined, }; + function handleOpenChange(o: boolean) { + setPopoverOpen(o); + if (o) { + setName(props.page.title); + setPath(props.page.path ?? "/"); + setMode("edit"); + } + } + + async function handleSave(e: React.FormEvent) { + e.preventDefault(); + if (!props.publicationUri || saving) return; + let trimmedPath = path.trim(); + if (!trimmedPath) return; + if (!trimmedPath.startsWith("/")) trimmedPath = "/" + trimmedPath; + let trimmedTitle = name.trim(); + setSaving(true); + let result = await updatePublicationPage({ + publication_uri: props.publicationUri, + page_id: props.page.id, + title: trimmedTitle, + path: trimmedPath, + }); + setSaving(false); + if (!result.success) return; + setPopoverOpen(false); + props.onUpdated({ title: trimmedTitle, path: trimmedPath }); + } + async function handleDelete() { if (!props.publicationUri || deleting) return; setDeleting(true); @@ -260,7 +307,7 @@ function SortableTab(props: { }); setDeleting(false); if (!result.success) return; - setConfirmOpen(false); + setPopoverOpen(false); props.onDeleted(); } @@ -299,39 +346,84 @@ function SortableTab(props: { > {props.page.title || props.page.path || "/"} - {props.canDelete && ( - - + } - title="Delete page?" > -
- This will permanently delete{" "} - - {props.page.title || props.page.path || "/"} - - . -
-
- setConfirmOpen(false)}> - Nevermind - - - {deleting ? "Deleting..." : "Delete"} - -
-
- )} + {mode === "edit" ? ( +
{ + if (e.key === "Tab") e.stopPropagation(); + }} + className="flex flex-col gap-2" + > + setName(e.currentTarget.value)} + autoFocus + /> + setPath(e.currentTarget.value)} + placeholder="/about" + /> +
+ {props.canDelete ? ( + setMode("confirm")} + > + Delete + + ) : ( +
+ )} + + {saving ? "Saving..." : "Save"} + +
+ + ) : ( +
+
+ This will permanently delete{" "} + + {props.page.title || props.page.path || "/"} + + . +
+
+ setMode("edit")}> + Cancel + + + {deleting ? "Deleting..." : "Delete"} + +
+
+ )} +
); } diff --git a/app/(app)/lish/[did]/[publication]/edit/[[...route]]/layout.tsx b/app/(app)/lish/[did]/[publication]/edit/[[...route]]/layout.tsx index 97d3788d..a262e887 100644 --- a/app/(app)/lish/[did]/[publication]/edit/[[...route]]/layout.tsx +++ b/app/(app)/lish/[did]/[publication]/edit/[[...route]]/layout.tsx @@ -12,7 +12,6 @@ import { PublicationSWRDataProvider } from "../../dashboard/PublicationSWRProvid import { PublicationPagesNav } from "./PublicationPagesNav"; import { PublicationEditHeader } from "./PublicationEditHeader"; import { PublicationHeader } from "../../PublicationHeader"; -import { PublicationStickyHeader } from "../../PublicationStickyHeader"; import { PublicationEditDirtyProvider } from "./dirtyContext"; export async function generateMetadata(props: { @@ -99,22 +98,20 @@ export default async function PublicationEditLayout(props: { publicationName={params.publication} />
- +
+ - } - > - + - +
{props.children}