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