a tool for shared writing and social publishing

add deleting posts and drafts

+218 -51
+49 -2
app/lish/[did]/[publication]/dashboard/DraftList.tsx
··· 2 2 3 3 import Link from "next/link"; 4 4 import { NewDraftSecondaryButton } from "./NewDraftButton"; 5 - import React from "react"; 5 + import React, { useState } from "react"; 6 6 import { usePublicationData } from "./PublicationSWRProvider"; 7 + import { Menu, MenuItem } from "components/Layout"; 8 + import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 9 + import { deleteDraft } from "./deleteDraft"; 7 10 8 11 export function DraftList() { 9 - let pub_data = usePublicationData(); 12 + let { data: pub_data } = usePublicationData(); 10 13 if (!pub_data) return null; 11 14 return ( 12 15 <div className="flex flex-col gap-4 pb-8 sm:pb-12"> ··· 26 29 } 27 30 28 31 function Draft(props: { id: string; title: string; description: string }) { 32 + let [open, setOpen] = useState(false); 29 33 return ( 30 34 <div className="flex flex-row gap-2 items-start"> 31 35 <Link ··· 40 44 )} 41 45 <div className="text-secondary italic">{props.description}</div> 42 46 </Link> 47 + <Menu 48 + open={open} 49 + onOpenChange={(o) => setOpen(o)} 50 + align="end" 51 + asChild 52 + trigger={ 53 + <button className="text-secondary hover:accent-primary border border-accent-2 rounded-md h-min w-min pt-2.5"> 54 + <MoreOptionsTiny className="rotate-90 h-min w-min " /> 55 + </button> 56 + } 57 + > 58 + <> 59 + <DeleteDraft id={props.id} /> 60 + </> 61 + </Menu> 43 62 </div> 44 63 ); 45 64 } 65 + 66 + export function DeleteDraft(props: { id: string }) { 67 + let { mutate } = usePublicationData(); 68 + let [state, setState] = useState<"normal" | "confirm">("normal"); 69 + 70 + return ( 71 + <MenuItem 72 + onSelect={async (e) => { 73 + if (state === "normal") { 74 + e.preventDefault(); 75 + return setState("confirm"); 76 + } 77 + await mutate((data) => { 78 + if (!data) return data; 79 + return { 80 + ...data, 81 + leaflets_in_publications: data.leaflets_in_publications.filter( 82 + (d) => d.leaflet !== props.id, 83 + ), 84 + }; 85 + }, false); 86 + await deleteDraft(props.id); 87 + }} 88 + > 89 + {state === "normal" ? "Delete Draft" : "Are you sure?"} 90 + </MenuItem> 91 + ); 92 + }
+2 -2
app/lish/[did]/[publication]/dashboard/PublicationSWRProvider.tsx
··· 31 31 32 32 export function usePublicationData() { 33 33 let { name, did } = useContext(PublicationContext); 34 - let { data } = useSWR( 34 + let { data, mutate } = useSWR( 35 35 "publication-data", 36 36 async () => 37 37 (await callRPC("get_publication_data", { publication_name: name, did })) 38 38 ?.result, 39 39 ); 40 - return data; 40 + return { data, mutate }; 41 41 }
+114 -46
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 3 3 import { AtUri } from "@atproto/syntax"; 4 4 import { PubLeafletDocument } from "lexicons/api"; 5 5 import { EditTiny } from "components/Icons/EditTiny"; 6 - import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 7 - import { Menu, MenuItem } from "components/Layout"; 8 6 9 7 import { usePublicationData } from "./PublicationSWRProvider"; 10 - import { Fragment } from "react"; 8 + import { Fragment, useState } from "react"; 11 9 import { useParams } from "next/navigation"; 12 10 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 + import { Menu, MenuItem } from "components/Layout"; 12 + import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 13 + import { deletePost } from "./deletePost"; 14 + import { mutate } from "swr"; 13 15 14 16 export function PublishedPostsList() { 15 - let publication = usePublicationData(); 17 + let { data: publication } = usePublicationData(); 16 18 let params = useParams(); 17 19 if (!publication) return null; 18 20 if (publication.documents_in_publications.length === 0) ··· 23 25 ); 24 26 return ( 25 27 <div className="publishedList w-full flex flex-col gap-4 pb-8 sm:pb-12"> 26 - {publication.documents_in_publications.map((doc) => { 27 - if (!doc.documents) return null; 28 - let leaflet = publication.leaflets_in_publications.find( 29 - (l) => doc.documents && l.doc === doc.documents.uri, 30 - ); 31 - let uri = new AtUri(doc.documents.uri); 32 - let record = doc.documents.data as PubLeafletDocument.Record; 28 + {publication.documents_in_publications 29 + .sort((a, b) => { 30 + let aRecord = a.documents?.data! as PubLeafletDocument.Record; 31 + let bRecord = b.documents?.data! as PubLeafletDocument.Record; 32 + const aDate = aRecord.publishedAt 33 + ? new Date(aRecord.publishedAt) 34 + : new Date(0); 35 + const bDate = bRecord.publishedAt 36 + ? new Date(bRecord.publishedAt) 37 + : new Date(0); 38 + return bDate.getTime() - aDate.getTime(); // Sort by most recent first 39 + }) 40 + .map((doc) => { 41 + if (!doc.documents) return null; 42 + let leaflet = publication.leaflets_in_publications.find( 43 + (l) => doc.documents && l.doc === doc.documents.uri, 44 + ); 45 + let uri = new AtUri(doc.documents.uri); 46 + let record = doc.documents.data as PubLeafletDocument.Record; 33 47 34 - return ( 35 - <Fragment key={doc.documents?.uri}> 36 - <div className="flex w-full "> 37 - <Link 38 - target="_blank" 39 - href={`${getPublicationURL(publication)}/${uri.rkey}`} 40 - className="publishedPost grow flex flex-col hover:!no-underline" 41 - > 42 - <h3 className="text-primary">{record.title}</h3> 43 - {record.description ? ( 44 - <p className="italic text-secondary">{record.description}</p> 45 - ) : null} 46 - {record.publishedAt ? ( 47 - <p className="text-sm text-tertiary pt-3"> 48 - Published{" "} 49 - {new Date(record.publishedAt).toLocaleDateString( 50 - undefined, 51 - { 52 - year: "numeric", 53 - month: "long", 54 - day: "2-digit", 55 - }, 56 - )} 57 - </p> 58 - ) : null} 59 - </Link> 60 - {leaflet && ( 61 - <Link className="pt-[6px]" href={`/${leaflet.leaflet}`}> 62 - <EditTiny /> 48 + return ( 49 + <Fragment key={doc.documents?.uri}> 50 + <div className="flex w-full "> 51 + <Link 52 + target="_blank" 53 + href={`${getPublicationURL(publication)}/${uri.rkey}`} 54 + className="publishedPost grow flex flex-col hover:!no-underline" 55 + > 56 + <h3 className="text-primary">{record.title}</h3> 57 + {record.description ? ( 58 + <p className="italic text-secondary"> 59 + {record.description} 60 + </p> 61 + ) : null} 62 + {record.publishedAt ? ( 63 + <p className="text-sm text-tertiary pt-3"> 64 + Published{" "} 65 + {new Date(record.publishedAt).toLocaleDateString( 66 + undefined, 67 + { 68 + year: "numeric", 69 + month: "long", 70 + day: "2-digit", 71 + }, 72 + )} 73 + </p> 74 + ) : null} 63 75 </Link> 64 - )} 65 - </div> 66 - <hr className="last:hidden border-border-light" /> 67 - </Fragment> 68 - ); 69 - })} 76 + <div className="flex justify-start align-top flex-row"> 77 + {leaflet && ( 78 + <Link className="pt-[6px]" href={`/${leaflet.leaflet}`}> 79 + <EditTiny /> 80 + </Link> 81 + )} 82 + <Options document_uri={doc.documents.uri} /> 83 + </div> 84 + </div> 85 + <hr className="last:hidden border-border-light" /> 86 + </Fragment> 87 + ); 88 + })} 70 89 </div> 71 90 ); 72 91 } 92 + 93 + let Options = (props: { document_uri: string }) => { 94 + return ( 95 + <Menu 96 + align="end" 97 + asChild 98 + trigger={ 99 + <button className="text-secondary hover:accent-primary border border-accent-2 rounded-md h-min w-min pt-2.5"> 100 + <MoreOptionsTiny className="rotate-90 h-min w-min " /> 101 + </button> 102 + } 103 + > 104 + <> 105 + <DeletePost document_uri={props.document_uri} /> 106 + </> 107 + </Menu> 108 + ); 109 + }; 110 + 111 + function DeletePost(props: { document_uri: string }) { 112 + let { mutate } = usePublicationData(); 113 + let [confirm, setConfirm] = useState(false); 114 + return ( 115 + <MenuItem 116 + onSelect={async (e) => { 117 + if (!confirm) { 118 + e.preventDefault(); 119 + setConfirm(true); 120 + return; 121 + } 122 + await mutate((data) => { 123 + if (!data) return data; 124 + return { 125 + ...data, 126 + leaflets_in_publications: data.leaflets_in_publications.filter( 127 + (l) => l.doc !== props.document_uri, 128 + ), 129 + documents_in_publications: data.documents_in_publications.filter( 130 + (d) => d.documents?.uri !== props.document_uri, 131 + ), 132 + }; 133 + }, false); 134 + await deletePost(props.document_uri); 135 + }} 136 + > 137 + {confirm ? "Delete Post" : "Are you sure?"} 138 + </MenuItem> 139 + ); 140 + }
+18
app/lish/[did]/[publication]/dashboard/deleteDraft.ts
··· 1 + "use server"; 2 + 3 + import { getIdentityData } from "actions/getIdentityData"; 4 + import { supabaseServerClient } from "supabase/serverClient"; 5 + import { revalidatePath } from "next/cache"; 6 + 7 + export async function deleteDraft(leaflet_id: string) { 8 + let identity = await getIdentityData(); 9 + if (!identity || !identity.atp_did) throw new Error("No Identity"); 10 + 11 + await Promise.all([ 12 + supabaseServerClient 13 + .from("leaflets_in_publications") 14 + .delete() 15 + .eq("leaflet", leaflet_id), 16 + ]); 17 + return revalidatePath("/lish/[did]/[publication]/dashboard", "layout"); 18 + }
+34
app/lish/[did]/[publication]/dashboard/deletePost.ts
··· 1 + "use server"; 2 + 3 + import { AtpBaseClient } from "lexicons/api"; 4 + import { getIdentityData } from "actions/getIdentityData"; 5 + import { createOauthClient } from "src/atproto-oauth"; 6 + import { AtUri } from "@atproto/syntax"; 7 + import { supabaseServerClient } from "supabase/serverClient"; 8 + import { revalidatePath } from "next/cache"; 9 + 10 + export async function deletePost(document_uri: string) { 11 + let identity = await getIdentityData(); 12 + if (!identity || !identity.atp_did) throw new Error("No Identity"); 13 + 14 + const oauthClient = await createOauthClient(); 15 + let credentialSession = await oauthClient.restore(identity.atp_did); 16 + let agent = new AtpBaseClient( 17 + credentialSession.fetchHandler.bind(credentialSession), 18 + ); 19 + let uri = new AtUri(document_uri); 20 + if (uri.host !== identity.atp_did) return; 21 + 22 + await Promise.all([ 23 + agent.pub.leaflet.document.delete({ 24 + repo: credentialSession.did, 25 + rkey: uri.rkey, 26 + }), 27 + supabaseServerClient.from("documents").delete().eq("uri", document_uri), 28 + supabaseServerClient 29 + .from("leaflets_in_publications") 30 + .delete() 31 + .eq("doc", document_uri), 32 + ]); 33 + return revalidatePath("/lish/[did]/[publication]/dashboard", "layout"); 34 + }
+1 -1
app/lish/createPub/UpdatePubForm.tsx
··· 134 134 }; 135 135 136 136 export function CustomDomainForm() { 137 - let pubData = usePublicationData(); 137 + let { data: pubData } = usePublicationData(); 138 138 if (!pubData) return null; 139 139 let record = pubData?.record as PubLeafletPublication.Record; 140 140 let [state, setState] = useState<