a tool for shared writing and social publishing

pass publication dashboard through swr

+154 -200
+2
app/api/rpc/[command]/route.ts
··· 9 9 import { Vercel } from "@vercel/sdk"; 10 10 import { get_domain_status } from "./domain_routes"; 11 11 import { get_leaflet_data } from "./get_leaflet_data"; 12 + import { get_publication_data } from "./get_publication_data"; 12 13 13 14 const client = postgres(process.env.DB_URL as string, { idle_timeout: 5 }); 14 15 let supabase = createClient<Database>( ··· 33 34 getFactsFromHomeLeaflets, 34 35 get_domain_status, 35 36 get_leaflet_data, 37 + get_publication_data, 36 38 ]; 37 39 export async function POST( 38 40 req: Request,
-3
app/lish/PostList.tsx
··· 1 1 "use client"; 2 2 import Link from "next/link"; 3 - import { Separator } from "components/Layout"; 4 3 import { Json } from "supabase/database.types"; 5 4 import { PubLeafletDocument } from "lexicons/api"; 6 - import { ButtonPrimary } from "components/Buttons"; 7 5 import { useIdentityData } from "components/IdentityProvider"; 8 - import { usePublicationRelationship } from "./[handle]/[publication]/usePublicationRelationship"; 9 6 import { useParams } from "next/navigation"; 10 7 import { AtUri } from "@atproto/syntax"; 11 8
+7 -20
app/lish/[handle]/[publication]/dashboard/DraftList.tsx
··· 1 1 "use client"; 2 2 3 - import { usePublicationRelationship } from "../usePublicationRelationship"; 4 - import { usePublicationContext } from "components/Providers/PublicationContext"; 5 3 import Link from "next/link"; 6 4 import { NewDraftSecondaryButton } from "./NewDraftButton"; 7 - import { MoreOptionsTiny } from "components/Icons/MoreOptionsTiny"; 8 - import { Menu, MenuItem } from "components/Layout"; 9 - import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 10 - import { DeleteSmall } from "components/Icons/DeleteSmall"; 11 5 import React from "react"; 6 + import { usePublicationData } from "./PublicationSWRProvider"; 12 7 13 - export function DraftList(props: { 14 - publication: string; 15 - drafts: { 16 - leaflet: string; 17 - description: string; 18 - title: string; 19 - }[]; 20 - }) { 21 - let rel = usePublicationRelationship(); 22 - let { publication } = usePublicationContext(); 23 - if (!publication) return null; 24 - if (!rel?.isAuthor) return null; 8 + export function DraftList() { 9 + let pub_data = usePublicationData(); 10 + console.log({ pub_data }); 11 + if (!pub_data) return null; 25 12 return ( 26 13 <div className="flex flex-col gap-4 pb-6"> 27 - <NewDraftSecondaryButton fullWidth publication={props.publication} /> 28 - {props.drafts.map((d) => { 14 + <NewDraftSecondaryButton fullWidth publication={pub_data?.name} /> 15 + {pub_data.leaflets_in_publications.map((d) => { 29 16 return ( 30 17 <React.Fragment key={d.leaflet}> 31 18 <Draft id={d.leaflet} {...d} />
+41
app/lish/[handle]/[publication]/dashboard/PublicationSWRProvider.tsx
··· 1 + "use client"; 2 + 3 + import type { GetPublicationDataReturnType } from "app/api/rpc/[command]/get_publication_data"; 4 + import { callRPC } from "app/api/rpc/client"; 5 + import { createContext, useContext } from "react"; 6 + import useSWR, { SWRConfig } from "swr"; 7 + 8 + const PublicationContext = createContext({ name: "", did: "" }); 9 + export function PublicationSWRDataProvider(props: { 10 + publication_name: string; 11 + publication_did: string; 12 + publication_data: GetPublicationDataReturnType["result"]; 13 + children: React.ReactNode; 14 + }) { 15 + return ( 16 + <PublicationContext 17 + value={{ name: props.publication_name, did: props.publication_did }} 18 + > 19 + <SWRConfig 20 + value={{ 21 + fallback: { 22 + "publication-data": props.publication_data, 23 + }, 24 + }} 25 + > 26 + {props.children} 27 + </SWRConfig> 28 + </PublicationContext> 29 + ); 30 + } 31 + 32 + export function usePublicationData() { 33 + let { name, did } = useContext(PublicationContext); 34 + let { data } = useSWR( 35 + "publication-data", 36 + async () => 37 + (await callRPC("get_publication_data", { publication_name: name, did })) 38 + ?.result, 39 + ); 40 + return data; 41 + }
+70
app/lish/[handle]/[publication]/dashboard/PublishedPostsLists.tsx
··· 1 + "use client"; 2 + import Link from "next/link"; 3 + import { AtUri } from "@atproto/syntax"; 4 + import { PubLeafletDocument } from "lexicons/api"; 5 + import { EditTiny } from "components/Icons/EditTiny"; 6 + import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 7 + import { Menu, MenuItem } from "components/Layout"; 8 + 9 + import { usePublicationData } from "./PublicationSWRProvider"; 10 + import { Fragment } from "react"; 11 + import { useParams } from "next/navigation"; 12 + 13 + export function PublishedPostsList() { 14 + let publication = usePublicationData(); 15 + let params = useParams(); 16 + if (!publication) return null; 17 + if (publication.documents_in_publications.length === 0) 18 + return ( 19 + <div className="italic text-tertiary w-full container text-center place-items-center flex flex-col gap-3 p-3"> 20 + Nothing's been published yet... 21 + </div> 22 + ); 23 + return ( 24 + <div className="publishedList w-full flex flex-col gap-4 pb-6"> 25 + {publication.documents_in_publications.map((doc) => { 26 + if (!doc.documents) return null; 27 + let leaflet = publication.leaflets_in_publications.find( 28 + (l) => doc.documents && l.doc === doc.documents.uri, 29 + ); 30 + let uri = new AtUri(doc.documents.uri); 31 + let record = doc.documents.data as PubLeafletDocument.Record; 32 + 33 + return ( 34 + <Fragment key={doc.documents?.uri}> 35 + <div className="flex w-full "> 36 + <Link 37 + href={`/lish/${params.handle}/${params.publication}/${uri.rkey}`} 38 + className="publishedPost grow flex flex-col hover:!no-underline" 39 + > 40 + <h3 className="text-primary">{record.title}</h3> 41 + {record.description ? ( 42 + <p className="italic text-secondary">{record.description}</p> 43 + ) : null} 44 + {record.publishedAt ? ( 45 + <p className="text-sm text-tertiary pt-3"> 46 + Published{" "} 47 + {new Date(record.publishedAt).toLocaleDateString( 48 + undefined, 49 + { 50 + year: "2-digit", 51 + month: "long", 52 + day: "2-digit", 53 + }, 54 + )} 55 + </p> 56 + ) : null} 57 + </Link> 58 + {leaflet && ( 59 + <Link className="pt-[6px]" href={`/${leaflet.leaflet}`}> 60 + <EditTiny /> 61 + </Link> 62 + )} 63 + </div> 64 + <hr className="last:hidden border-border-light" /> 65 + </Fragment> 66 + ); 67 + })} 68 + </div> 69 + ); 70 + }
-30
app/lish/[handle]/[publication]/dashboard/layout.tsx
··· 1 - import { IdResolver } from "@atproto/identity"; 2 - import { PublicationContextProvider } from "components/Providers/PublicationContext"; 3 - import { supabaseServerClient } from "supabase/serverClient"; 4 - 5 - const idResolver = new IdResolver(); 6 - export default async function PublicationLayout(props: { 7 - children: React.ReactNode; 8 - params: Promise<{ 9 - publication: string; 10 - handle: string; 11 - }>; 12 - }) { 13 - let did = await idResolver.handle.resolve((await props.params).handle); 14 - if (!did) return <>{props.children}</>; 15 - let { data: publication } = await supabaseServerClient 16 - .from("publications") 17 - .select( 18 - "*, documents_in_publications(documents(*)), leaflets_in_publications(*, permission_tokens(*, permission_token_rights(*), custom_domain_routes!custom_domain_routes_edit_permission_token_fkey(*) ))", 19 - ) 20 - .eq("identity_did", did) 21 - .eq("name", decodeURIComponent((await props.params).publication)) 22 - .single(); 23 - 24 - if (!publication) return <>{props.children}</>; 25 - return ( 26 - <PublicationContextProvider publication={publication}> 27 - {props.children} 28 - </PublicationContextProvider> 29 - ); 30 - }
+34 -99
app/lish/[handle]/[publication]/dashboard/page.tsx
··· 11 11 import { getIdentityData } from "actions/getIdentityData"; 12 12 import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 13 13 import { Actions } from "./Actions"; 14 - import Link from "next/link"; 15 - import { AtUri } from "@atproto/syntax"; 16 - import { PubLeafletDocument } from "lexicons/api"; 17 14 import React from "react"; 18 - import { EditTiny } from "components/Icons/EditTiny"; 19 - import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 20 - import { Menu, MenuItem } from "components/Layout"; 21 15 import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 16 + import { PublicationSWRDataProvider } from "./PublicationSWRProvider"; 17 + import { PublishedPostsList } from "./PublishedPostsLists"; 22 18 23 19 const idResolver = new IdResolver(); 24 20 ··· 60 56 61 57 try { 62 58 return ( 63 - <ThemeProvider entityID={null}> 64 - <div className="w-screen h-screen flex place-items-center bg-[#FDFCFA]"> 65 - <div className="relative max-w-prose w-full h-full mx-auto flex sm:flex-row flex-col sm:items-stretch sm:px-6"> 66 - <div className="w-12 relative"> 67 - <Sidebar className="mt-6 p-2"> 68 - <Actions publication={publication.uri} /> 69 - </Sidebar> 59 + <PublicationSWRDataProvider 60 + publication_did={did} 61 + publication_name={publication.name} 62 + publication_data={publication} 63 + > 64 + <ThemeProvider entityID={null}> 65 + <div className="w-screen h-screen flex place-items-center bg-[#FDFCFA]"> 66 + <div className="relative max-w-prose w-full h-full mx-auto flex sm:flex-row flex-col sm:items-stretch sm:px-6"> 67 + <div className="w-12 relative"> 68 + <Sidebar className="mt-6 p-2"> 69 + <Actions publication={publication.uri} /> 70 + </Sidebar> 71 + </div> 72 + <div 73 + className={`h-full overflow-y-scroll pt-4 sm:pl-5 sm:pt-9 w-full`} 74 + > 75 + <PublicationDashboard 76 + name={publication.name} 77 + tabs={{ 78 + Drafts: <DraftList />, 79 + Published: <PublishedPostsList />, 80 + }} 81 + defaultTab={"Drafts"} 82 + /> 83 + </div> 84 + <Media mobile> 85 + <Footer> 86 + <Actions publication={publication.uri} /> 87 + </Footer> 88 + </Media> 70 89 </div> 71 - <div 72 - className={`h-full overflow-y-scroll pt-4 sm:pl-5 sm:pt-9 w-full`} 73 - > 74 - <PublicationDashboard 75 - name={publication.name} 76 - tabs={{ 77 - Drafts: ( 78 - <DraftList 79 - publication={publication.uri} 80 - drafts={publication.leaflets_in_publications.filter( 81 - (p) => !p.doc, 82 - )} 83 - /> 84 - ), 85 - Published: 86 - publication.documents_in_publications.length === 0 ? ( 87 - <div className="italic text-tertiary w-full container text-center place-items-center flex flex-col gap-3 p-3"> 88 - Nothing's been published yet... 89 - </div> 90 - ) : ( 91 - <div className="publishedList w-full flex flex-col gap-4 pb-6"> 92 - {publication.documents_in_publications.map((doc) => { 93 - if (!doc.documents) return null; 94 - let leaflet = 95 - publication.leaflets_in_publications.find( 96 - (l) => 97 - doc.documents && l.doc === doc.documents.uri, 98 - ); 99 - let uri = new AtUri(doc.documents.uri); 100 - let record = doc.documents 101 - .data as PubLeafletDocument.Record; 102 - 103 - return ( 104 - <React.Fragment key={doc.documents?.uri}> 105 - <div className="flex w-full "> 106 - <Link 107 - href={`/lish/${params.handle}/${params.publication}/${uri.rkey}`} 108 - className="publishedPost grow flex flex-col hover:!no-underline" 109 - > 110 - <h3 className="text-primary"> 111 - {record.title} 112 - </h3> 113 - {record.description ? ( 114 - <p className="italic text-secondary"> 115 - {record.description} 116 - </p> 117 - ) : null} 118 - {record.publishedAt ? ( 119 - <p className="text-sm text-tertiary pt-3"> 120 - Published{" "} 121 - {new Date( 122 - record.publishedAt, 123 - ).toLocaleDateString(undefined, { 124 - year: "2-digit", 125 - month: "long", 126 - day: "2-digit", 127 - })} 128 - </p> 129 - ) : null} 130 - </Link> 131 - {leaflet && ( 132 - <Link 133 - className="pt-[6px]" 134 - href={`/${leaflet.leaflet}`} 135 - > 136 - <EditTiny /> 137 - </Link> 138 - )} 139 - </div> 140 - <hr className="last:hidden border-border-light" /> 141 - </React.Fragment> 142 - ); 143 - })} 144 - </div> 145 - ), 146 - }} 147 - defaultTab={"Drafts"} 148 - /> 149 - </div> 150 - <Media mobile> 151 - <Footer> 152 - <Actions publication={publication.uri} /> 153 - </Footer> 154 - </Media> 155 90 </div> 156 - </div> 157 - </ThemeProvider> 91 + </ThemeProvider> 92 + </PublicationSWRDataProvider> 158 93 ); 159 94 } catch (e) { 160 95 console.log(e);
-16
app/lish/[handle]/[publication]/usePublicationRelationship.ts
··· 1 - import { AtUri } from "@atproto/syntax"; 2 - import { useIdentityData } from "components/IdentityProvider"; 3 - import { usePublicationContext } from "components/Providers/PublicationContext"; 4 - 5 - export const usePublicationRelationship = () => { 6 - let identity = useIdentityData(); 7 - let publication = usePublicationContext(); 8 - if (!publication.publication) return null; 9 - let pubUri = new AtUri(publication.publication.uri); 10 - let isAuthor = 11 - identity.identity?.atp_did && pubUri.hostname === identity.identity.atp_did; 12 - let isSubscribed = identity.identity?.subscribers_to_publications.find( 13 - (p) => p.publication === publication.publication?.uri, 14 - ); 15 - return { isAuthor, isSubscribed }; 16 - };
-32
components/Providers/PublicationContext.tsx
··· 1 - "use client"; 2 - import { createContext, useContext, ReactNode } from "react"; 3 - 4 - interface PublicationContextType { 5 - publication: { uri: string; name: string } | null; 6 - } 7 - 8 - const PublicationContext = createContext<PublicationContextType | undefined>( 9 - undefined, 10 - ); 11 - 12 - export function PublicationContextProvider({ 13 - children, 14 - publication, 15 - }: { 16 - children: ReactNode; 17 - publication: PublicationContextType["publication"]; 18 - }) { 19 - return ( 20 - <PublicationContext.Provider value={{ publication }}> 21 - {children} 22 - </PublicationContext.Provider> 23 - ); 24 - } 25 - 26 - export function usePublicationContext() { 27 - const context = useContext(PublicationContext); 28 - if (context === undefined) { 29 - throw new Error("usePublication must be used within a PublicationProvider"); 30 - } 31 - return context; 32 - }