a tool for shared writing and social publishing

wire up draft metadata

+135 -58
+26
actions/publications/updateLeafletDraftMetadata.ts
··· 1 + "use server"; 2 + 3 + import { getIdentityData } from "actions/getIdentityData"; 4 + import { supabaseServerClient } from "supabase/serverClient"; 5 + 6 + export async function updateLeafletDraftMetadata( 7 + leafletID: string, 8 + publication_uri: string, 9 + title: string, 10 + description: string, 11 + ) { 12 + let identity = await getIdentityData(); 13 + if (!identity?.atp_did) return null; 14 + let { data: publication } = await supabaseServerClient 15 + .from("publications") 16 + .select() 17 + .eq("uri", publication_uri) 18 + .single(); 19 + if (!publication || publication.identity_did !== identity.atp_did) 20 + return null; 21 + await supabaseServerClient 22 + .from("leaflets_in_publications") 23 + .update({ title, description }) 24 + .eq("leaflet", leafletID) 25 + .eq("publication", publication_uri); 26 + }
+8 -17
app/lish/[handle]/[publication]/DraftList.tsx
··· 8 8 9 9 export function DraftList(props: { 10 10 publication: string; 11 - drafts: { id: string; initialFacts: Fact<any>[]; root_entity: string }[]; 11 + drafts: { 12 + leaflet: string; 13 + description: string; 14 + title: string; 15 + }[]; 12 16 }) { 13 17 let rel = usePublicationRelationship(); 14 18 let { publication } = usePublicationContext(); ··· 18 22 <div className="flex flex-col gap-2"> 19 23 <NewDraftSecondaryButton publication={props.publication} /> 20 24 {props.drafts.map((d) => { 21 - return ( 22 - <ReplicacheProvider 23 - key={d.id} 24 - rootEntity={d.root_entity} 25 - initialFacts={d.initialFacts} 26 - token={{ 27 - ...d, 28 - permission_token_rights: [], 29 - }} 30 - name={d.id} 31 - > 32 - <Draft id={d.id} /> 33 - </ReplicacheProvider> 34 - ); 25 + return <Draft id={d.leaflet} key={d.leaflet} {...d} />; 35 26 })} 36 27 </div> 37 28 ); 38 29 } 39 30 40 - function Draft(props: { id: string }) { 31 + function Draft(props: { id: string; title: string }) { 41 32 return ( 42 33 <Link key={props.id} href={`/${props.id}`}> 43 - {props.id} 34 + <h3>{props.title}</h3> 44 35 </Link> 45 36 ); 46 37 }
+1 -24
app/lish/[handle]/[publication]/page.tsx
··· 11 11 import { PublicationDashboard } from "./PublicationDashboard"; 12 12 import { DraftList } from "./DraftList"; 13 13 import { NewDraftActionButton } from "./NewDraftButton"; 14 - import { use } from "react"; 15 - import { IdentityContext } from "components/IdentityProvider"; 16 14 import { getIdentityData } from "actions/getIdentityData"; 17 15 18 16 const idResolver = new IdResolver(); ··· 61 59 if (!publication || identity.atp_did !== publication.identity_did) 62 60 return <PubNotFound />; 63 61 64 - let all_facts = await supabaseServerClient.rpc("get_facts_for_roots", { 65 - max_depth: 2, 66 - roots: publication.leaflets_in_publications.map( 67 - (l) => l.permission_tokens?.root_entity!, 68 - ), 69 - }); 70 - let facts = 71 - all_facts.data?.reduce( 72 - (acc, fact) => { 73 - if (!acc[fact.root_id]) acc[fact.root_id] = []; 74 - acc[fact.root_id].push( 75 - fact as unknown as Fact<keyof typeof Attributes>, 76 - ); 77 - return acc; 78 - }, 79 - {} as { [key: string]: Fact<keyof typeof Attributes>[] }, 80 - ) || {}; 81 - 82 62 try { 83 63 return ( 84 64 <div className="relative max-w-screen-lg w-full h-full mx-auto flex sm:flex-row flex-col sm:items-stretch sm:px-6"> ··· 92 72 Drafts: ( 93 73 <DraftList 94 74 publication={publication.uri} 95 - drafts={publication.leaflets_in_publications.map((d) => ({ 96 - ...d.permission_tokens!, 97 - initialFacts: facts[d.permission_tokens?.root_entity!], 98 - }))} 75 + drafts={publication.leaflets_in_publications} 99 76 /> 100 77 ), 101 78 Published: <div>none yet lol</div>,
+1
components/PageSWRDataProvider.tsx
··· 59 59 let { data, mutate } = useLeafletData(); 60 60 return { 61 61 data: data?.data?.leaflets_in_publications, 62 + mutate, 62 63 }; 63 64 } 64 65 export function useLeafletDomains() {
+79
components/Pages/PublicationMetadata.tsx
··· 1 + import Link from "next/link"; 2 + import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 3 + import { Input } from "components/Input"; 4 + import { useEffect, useState } from "react"; 5 + import { useDebouncedEffect } from "src/hooks/useDebouncedEffect"; 6 + import { updateLeafletDraftMetadata } from "actions/publications/updateLeafletDraftMetadata"; 7 + import { useReplicache } from "src/replicache"; 8 + import { useIdentityData } from "components/IdentityProvider"; 9 + export const PublicationMetadata = ({ 10 + cardBorderHidden, 11 + }: { 12 + cardBorderHidden: boolean; 13 + }) => { 14 + let { permission_token } = useReplicache(); 15 + let { identity } = useIdentityData(); 16 + let { data: publicationData, mutate } = useLeafletPublicationData(); 17 + let pub = publicationData?.[0]; 18 + let [titleState, setTitleState] = useState(pub?.title || ""); 19 + let [descriptionState, setDescriptionState] = useState(pub?.title || ""); 20 + 21 + useEffect(() => { 22 + setTitleState(pub?.title || ""); 23 + setDescriptionState(pub?.description || ""); 24 + }, [pub]); 25 + useDebouncedEffect( 26 + async () => { 27 + if (!pub || !pub.publications) return; 28 + if (pub.title === titleState && pub.description === descriptionState) 29 + return; 30 + await updateLeafletDraftMetadata( 31 + permission_token.id, 32 + pub.publications?.uri, 33 + titleState, 34 + descriptionState, 35 + ); 36 + mutate(); 37 + }, 38 + 1000, 39 + [pub, titleState, descriptionState, permission_token], 40 + ); 41 + if (!pub || !pub.publications) return null; 42 + 43 + return ( 44 + <div 45 + className={`flex flex-col px-3 sm:px-4 pb-4 sm:pb-6 ${cardBorderHidden ? "sm:pt-4 pt-0" : "sm:pt-6 pt-2"}`} 46 + > 47 + <Link 48 + href={`/lish/${identity?.resolved_did?.alsoKnownAs?.[0].slice(5)}/${pub.publications.name}`} 49 + className="text-accent-contrast font-bold hover:no-underline" 50 + > 51 + {pub.publications?.name} 52 + </Link> 53 + <Input 54 + className="text-xl font-bold outline-none" 55 + value={titleState} 56 + onChange={(e) => { 57 + setTitleState(e.currentTarget.value); 58 + }} 59 + placeholder="Untitled" 60 + /> 61 + <textarea 62 + rows={2} 63 + placeholder="description (optional)" 64 + className="italic text-secondary outline-none" 65 + value={descriptionState} 66 + onChange={(e) => { 67 + setDescriptionState(e.currentTarget.value); 68 + }} 69 + /> 70 + {pub.doc ? ( 71 + <p>Published!</p> 72 + ) : ( 73 + <p className="text-sm text-tertiary">Draft</p> 74 + )} 75 + </div> 76 + ); 77 + }; 78 + 79 + const Title = () => {};
+2 -17
components/Pages/index.tsx
··· 34 34 import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 35 35 import { PaintSmall } from "components/Icons/PaintSmall"; 36 36 import { ShareSmall } from "components/Icons/ShareSmall"; 37 - import Link from "next/link"; 37 + import { PublicationMetadata } from "./PublicationMetadata"; 38 38 39 39 export function Pages(props: { rootPage: string }) { 40 40 let rootPage = useEntity(props.rootPage, "root/page")[0]; ··· 231 231 }} 232 232 /> 233 233 ) : null} 234 - <div 235 - className={`flex flex-col px-3 sm:px-4 pb-4 sm:pb-6 ${cardBorderHidden?.data.value ? "sm:pt-4 pt-0" : "sm:pt-6 pt-2"}`} 236 - > 237 - <Link 238 - href="/" 239 - className="text-accent-contrast font-bold hover:no-underline" 240 - > 241 - Publication Name 242 - </Link> 243 - <h2 className="">Titles Are Cool</h2> 244 - <p className="italic text-secondary"> 245 - This is a description! What happens if it's just a little but longer, 246 - since it's nice to be able to say a few more words... 247 - </p> 248 - <p className="text-sm text-tertiary">Draft</p> 249 - </div> 234 + <PublicationMetadata cardBorderHidden={!!cardBorderHidden?.data.value} /> 250 235 <Blocks entityID={props.entityID} /> 251 236 {/* we handle page bg in this sepate div so that 252 237 we can apply an opacity the background image
+18
src/hooks/useDebouncedEffect.ts
··· 1 + import { useEffect, useState, useRef } from "react"; 2 + 3 + export function useDebouncedEffect( 4 + fn: () => void, 5 + delay: number, 6 + deps: React.DependencyList = [], 7 + ): void { 8 + useEffect(() => { 9 + const handler = setTimeout(() => { 10 + fn(); 11 + }, delay); 12 + 13 + return () => { 14 + clearTimeout(handler); 15 + }; 16 + // eslint-disable-next-line react-hooks/exhaustive-deps 17 + }, [...deps, delay]); 18 + }