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
started the reader
cozylittle.house
5 months ago
bd92da54
8b7895fc
+340
-45
13 changed files
expand all
collapse all
unified
split
app
discover
page.tsx
home
HomeEmpty
HomeEmpty.tsx
HomeLayout.tsx
lish
[did]
[publication]
dashboard
PublicationDashboard.tsx
reader
ReaderContent.tsx
SubscriptionsContent.tsx
page.tsx
components
ActionBar
Navigation.tsx
Publications.tsx
Icons
ExternalLinkTiny.tsx
PageHeader.tsx
PageLayouts
DashboardLayout.tsx
feeds
index.ts
+1
-1
app/discover/page.tsx
···
40
40
<div className="w-full h-full mx-auto bg-[#FDFCFA]">
41
41
<DashboardLayout
42
42
id="discover"
43
43
-
hasBackgroundImage={false}
43
43
+
cardBorderHidden={false}
44
44
currentPage="discover"
45
45
defaultTab="default"
46
46
actions={null}
+2
-2
app/home/HomeEmpty/HomeEmpty.tsx
···
88
88
export const DiscoverBanner = (props: { small?: boolean }) => {
89
89
return (
90
90
<div
91
91
-
className={`accent-container flex sm:py-2 gap-4 items-center ${props.small ? "items-start p-2 text-sm font-normal" : "items-center p-4"}`}
91
91
+
className={`accent-container flex sm:py-2 gap-2 items-center ${props.small ? "items-start p-2 text-sm font-normal" : "items-center p-4"}`}
92
92
>
93
93
{props.small ? (
94
94
-
<DiscoverSmall className="shrink-0" />
94
94
+
<DiscoverSmall className="shrink-0 text-accent-contrast" />
95
95
) : (
96
96
<div className="w-[64px] mx-auto">
97
97
<DiscoverIllo />
+1
-2
app/home/HomeLayout.tsx
···
95
95
return (
96
96
<DashboardLayout
97
97
id="home"
98
98
-
hasBackgroundImage={hasBackgroundImage}
98
98
+
cardBorderHidden={cardBorderHidden}
99
99
currentPage="home"
100
100
defaultTab="home"
101
101
actions={<Actions />}
···
192
192
{leaflets.filter((l) => !!l.token.leaflets_in_publications).length ===
193
193
0 && <PublicationBanner small />}
194
194
<DiscoverBanner small />
195
195
-
<div className="spacer h-8 w-full bg-transparent shrink-0 " />
196
195
</>
197
196
);
198
197
}
+1
-1
app/lish/[did]/[publication]/dashboard/PublicationDashboard.tsx
···
39
39
return (
40
40
<DashboardLayout
41
41
id={publication.uri}
42
42
-
hasBackgroundImage={!!record?.theme?.backgroundImage}
42
42
+
cardBorderHidden={!!record.theme?.showPageBackground}
43
43
defaultTab="Drafts"
44
44
tabs={{
45
45
Drafts: {
+92
app/reader/ReaderContent.tsx
···
1
1
+
"use client";
2
2
+
import { ShareSmall } from "components/Icons/ShareSmall";
3
3
+
import { Separator } from "components/Layout";
4
4
+
import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
5
5
+
import Link from "next/link";
6
6
+
7
7
+
export const ReaderContent = (props: { root_entity: string }) => {
8
8
+
let cardBorderHidden = useCardBorderHidden(props.root_entity);
9
9
+
return (
10
10
+
<div className="flex flex-col gap-3">
11
11
+
{dummyPosts.map((p) => (
12
12
+
<Post {...p} cardBorderHidden={true} />
13
13
+
))}
14
14
+
</div>
15
15
+
);
16
16
+
};
17
17
+
18
18
+
const Post = (props: {
19
19
+
title: string;
20
20
+
description: string;
21
21
+
date: string;
22
22
+
read: boolean;
23
23
+
author: string;
24
24
+
pub: string;
25
25
+
cardBorderHidden: boolean;
26
26
+
}) => {
27
27
+
return (
28
28
+
<div
29
29
+
className={`flex flex-col gap-0 ${props.cardBorderHidden ? "bg-bg-page" : "bg-bg-leaflet"} p-3 rounded-lg border border-border-light`}
30
30
+
>
31
31
+
<div
32
32
+
className={`${props.cardBorderHidden ? "bg-transparent" : "bg-bg-page px-3 py-2"} rounded-md`}
33
33
+
>
34
34
+
<div className="flex justify-between gap-2">
35
35
+
<Link
36
36
+
href={"/"}
37
37
+
className="text-accent-contrast font-bold no-underline text-sm "
38
38
+
>
39
39
+
{props.pub}
40
40
+
</Link>
41
41
+
<button className="text-tertiary">{/*<ShareSmall />*/}</button>
42
42
+
</div>
43
43
+
<h3 className="truncate">{props.title}</h3>
44
44
+
45
45
+
<p className="text-secondary">{props.description}</p>
46
46
+
<div className="flex gap-2 text-sm text-tertiary items-center pt-3">
47
47
+
<div className="flex gap-[6px] items-center">
48
48
+
<div className="bg-test rounded-full h-4 w-4" />
49
49
+
{props.author}
50
50
+
</div>
51
51
+
<Separator classname="h-4 !min-h-0" />
52
52
+
{props.date}
53
53
+
</div>
54
54
+
</div>
55
55
+
</div>
56
56
+
);
57
57
+
};
58
58
+
59
59
+
let dummyPosts: {
60
60
+
title: string;
61
61
+
description: string;
62
62
+
date: string;
63
63
+
read: boolean;
64
64
+
author: string;
65
65
+
pub: string;
66
66
+
}[] = [
67
67
+
{
68
68
+
title: "First Post",
69
69
+
description: "this is a description",
70
70
+
date: "Oct 2",
71
71
+
read: false,
72
72
+
author: "jared",
73
73
+
pub: "a warm space",
74
74
+
},
75
75
+
{
76
76
+
title: "This is a second Tost",
77
77
+
description: "It has another description, as you can see",
78
78
+
date: "Oct 2",
79
79
+
read: false,
80
80
+
author: "celine",
81
81
+
pub: "Celine's Super Soliloquy",
82
82
+
},
83
83
+
{
84
84
+
title: "A Third Post, A Burnt Toast",
85
85
+
description:
86
86
+
"If the first post is bread, the second is toast, and inevitably the third is a plate of charcoal.",
87
87
+
date: "Oct 2",
88
88
+
read: false,
89
89
+
author: "brendan",
90
90
+
pub: "Scraps",
91
91
+
},
92
92
+
];
+3
app/reader/SubscriptionsContent.tsx
···
1
1
+
export const SubscriptionsContent = () => {
2
2
+
return <div>subs here</div>;
3
3
+
};
+147
app/reader/page.tsx
···
1
1
+
import { cookies } from "next/headers";
2
2
+
import { Fact, ReplicacheProvider, useEntity } from "src/replicache";
3
3
+
import type { Attribute } from "src/replicache/attributes";
4
4
+
import {
5
5
+
ThemeBackgroundProvider,
6
6
+
ThemeProvider,
7
7
+
} from "components/ThemeManager/ThemeProvider";
8
8
+
import { EntitySetProvider } from "components/EntitySetProvider";
9
9
+
import { createIdentity } from "actions/createIdentity";
10
10
+
import { drizzle } from "drizzle-orm/node-postgres";
11
11
+
import { IdentitySetter } from "app/home/IdentitySetter";
12
12
+
import { getIdentityData } from "actions/getIdentityData";
13
13
+
import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets";
14
14
+
import { supabaseServerClient } from "supabase/serverClient";
15
15
+
import { pool } from "supabase/pool";
16
16
+
17
17
+
import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
18
18
+
import { DashboardLayout } from "components/PageLayouts/DashboardLayout";
19
19
+
import { ReaderContent } from "./ReaderContent";
20
20
+
import { SubscriptionsContent } from "./SubscriptionsContent";
21
21
+
22
22
+
export default async function Reader(props: {}) {
23
23
+
let cookieStore = await cookies();
24
24
+
let auth_res = await getIdentityData();
25
25
+
let identity: string | undefined;
26
26
+
if (auth_res) identity = auth_res.id;
27
27
+
else identity = cookieStore.get("identity")?.value;
28
28
+
let needstosetcookie = false;
29
29
+
if (!identity) {
30
30
+
const client = await pool.connect();
31
31
+
const db = drizzle(client);
32
32
+
let newIdentity = await createIdentity(db);
33
33
+
client.release();
34
34
+
identity = newIdentity.id;
35
35
+
needstosetcookie = true;
36
36
+
}
37
37
+
38
38
+
async function setCookie() {
39
39
+
"use server";
40
40
+
41
41
+
(await cookies()).set("identity", identity as string, {
42
42
+
sameSite: "strict",
43
43
+
});
44
44
+
}
45
45
+
46
46
+
let permission_token = auth_res?.home_leaflet;
47
47
+
if (!permission_token) {
48
48
+
let res = await supabaseServerClient
49
49
+
.from("identities")
50
50
+
.select(
51
51
+
`*,
52
52
+
permission_tokens!identities_home_page_fkey(*, permission_token_rights(*))
53
53
+
`,
54
54
+
)
55
55
+
.eq("id", identity)
56
56
+
.single();
57
57
+
permission_token = res.data?.permission_tokens;
58
58
+
}
59
59
+
60
60
+
if (!permission_token)
61
61
+
return (
62
62
+
<NotFoundLayout>
63
63
+
<p className="font-bold">Sorry, we can't find this page!</p>
64
64
+
<p>
65
65
+
This may be a glitch on our end. If the issue persists please{" "}
66
66
+
<a href="mailto:contact@leaflet.pub">send us a note</a>.
67
67
+
</p>
68
68
+
</NotFoundLayout>
69
69
+
);
70
70
+
let [homeLeafletFacts, allLeafletFacts] = await Promise.all([
71
71
+
supabaseServerClient.rpc("get_facts", {
72
72
+
root: permission_token.root_entity,
73
73
+
}),
74
74
+
auth_res
75
75
+
? getFactsFromHomeLeaflets.handler(
76
76
+
{
77
77
+
tokens: auth_res.permission_token_on_homepage.map(
78
78
+
(r) => r.permission_tokens.root_entity,
79
79
+
),
80
80
+
},
81
81
+
{ supabase: supabaseServerClient },
82
82
+
)
83
83
+
: undefined,
84
84
+
]);
85
85
+
let initialFacts =
86
86
+
(homeLeafletFacts.data as unknown as Fact<Attribute>[]) || [];
87
87
+
let root_entity = permission_token.root_entity;
88
88
+
89
89
+
if (!auth_res?.atp_did) return;
90
90
+
let { data: publications } = await supabaseServerClient
91
91
+
.from("publication_subscriptions")
92
92
+
.select(`publications(*, documents_in_publications(documents(*)))`)
93
93
+
.eq("identity", auth_res?.atp_did);
94
94
+
95
95
+
let subbedPubs = publications?.map((pub) => {
96
96
+
let subbedPubsArr: string[] = [];
97
97
+
pub.publications?.name && subbedPubsArr.push(pub.publications?.name);
98
98
+
return subbedPubsArr;
99
99
+
});
100
100
+
101
101
+
console.log(subbedPubs);
102
102
+
103
103
+
return (
104
104
+
<ReplicacheProvider
105
105
+
rootEntity={root_entity}
106
106
+
token={permission_token}
107
107
+
name={root_entity}
108
108
+
initialFacts={initialFacts}
109
109
+
>
110
110
+
<IdentitySetter cb={setCookie} call={needstosetcookie} />
111
111
+
<EntitySetProvider
112
112
+
set={permission_token.permission_token_rights[0].entity_set}
113
113
+
>
114
114
+
<ThemeProvider entityID={root_entity}>
115
115
+
<ThemeBackgroundProvider entityID={root_entity}>
116
116
+
<DashboardLayout
117
117
+
id="reader"
118
118
+
cardBorderHidden={false}
119
119
+
currentPage="discover"
120
120
+
defaultTab="reader"
121
121
+
actions={null}
122
122
+
tabs={{
123
123
+
reader: {
124
124
+
controls: <FocusToggle />,
125
125
+
content: <ReaderContent root_entity={root_entity} />,
126
126
+
},
127
127
+
subs: {
128
128
+
controls: null,
129
129
+
content: <SubscriptionsContent />,
130
130
+
},
131
131
+
discover: {
132
132
+
controls: null,
133
133
+
content: <></>,
134
134
+
href: "/discover",
135
135
+
},
136
136
+
}}
137
137
+
/>
138
138
+
</ThemeBackgroundProvider>
139
139
+
</ThemeProvider>
140
140
+
</EntitySetProvider>
141
141
+
</ReplicacheProvider>
142
142
+
);
143
143
+
}
144
144
+
145
145
+
const FocusToggle = () => {
146
146
+
return <div className="grow flex justify-end">focus</div>;
147
147
+
};
+19
-13
components/ActionBar/Navigation.tsx
···
7
7
import { PublicationButtons } from "./Publications";
8
8
import { Popover } from "components/Popover";
9
9
import { MenuSmall } from "components/Icons/MenuSmall";
10
10
+
import {
11
11
+
ReaderReadSmall,
12
12
+
ReaderUnreadSmall,
13
13
+
} from "components/Icons/ReaderSmall";
10
14
11
15
export type navPages = "home" | "reader" | "pub" | "discover";
12
16
···
110
114
};
111
115
112
116
const ReaderButton = (props: { current?: boolean }) => {
113
113
-
// let readerUnreads = true;
114
114
-
// let subs = false;
117
117
+
let readerUnreads = true;
118
118
+
let subs = true;
115
119
116
116
-
// if (subs)
117
117
-
// return (
118
118
-
// <ActionButton
119
119
-
// nav
120
120
-
// icon={readerUnreads ? <ReaderUnreadSmall /> : <ReaderReadSmall />}
121
121
-
// label="Reader"
122
122
-
// className={`
123
123
-
// ${readerUnreads ? "text-accent-contrast! border-accent-contrast" : props.current ? "bg-border-light! border-border" : ""}
124
124
-
// `}
125
125
-
// />
126
126
-
// );
120
120
+
if (subs)
121
121
+
return (
122
122
+
<Link href={"/reader"} className="hover:no-underline!">
123
123
+
<ActionButton
124
124
+
nav
125
125
+
icon={readerUnreads ? <ReaderUnreadSmall /> : <ReaderReadSmall />}
126
126
+
label="Reader"
127
127
+
className={`
128
128
+
${readerUnreads ? "text-accent-contrast! border-accent-contrast" : props.current ? "bg-border-light! border-border" : ""}
129
129
+
`}
130
130
+
/>
131
131
+
</Link>
132
132
+
);
127
133
return (
128
134
<Link href={"/discover"} className="hover:no-underline!">
129
135
<ActionButton
-1
components/ActionBar/Publications.tsx
···
24
24
{identity.publications?.map((d) => {
25
25
// console.log("thisURI : " + d.uri);
26
26
// console.log("currentURI : " + props.currentPubUri);
27
27
-
console.log(d.uri === props.currentPubUri);
28
27
29
28
return (
30
29
<PublicationOption
+19
components/Icons/ExternalLinkTiny.tsx
···
1
1
+
import { Props } from "./Props";
2
2
+
3
3
+
export const ExternalLinkTiny = (props: Props) => {
4
4
+
return (
5
5
+
<svg
6
6
+
width="16"
7
7
+
height="16"
8
8
+
viewBox="0 0 16 16"
9
9
+
fill="none"
10
10
+
xmlns="http://www.w3.org/2000/svg"
11
11
+
{...props}
12
12
+
>
13
13
+
<path
14
14
+
d="M4.94336 2.26074C5.22793 2.3192 5.44238 2.57118 5.44238 2.87305C5.44227 3.17482 5.22788 3.42693 4.94336 3.48535L4.81738 3.49805H4.37305C3.61366 3.49805 2.99805 4.11366 2.99805 4.87305V11.627C2.99818 12.3862 3.61374 13.002 4.37305 13.002H11.127C11.8863 13.002 12.5018 12.3862 12.502 11.627V11.1055C12.5021 10.7605 12.7819 10.4805 13.127 10.4805C13.472 10.4805 13.7518 10.7605 13.752 11.1055V11.627C13.7518 13.0766 12.5766 14.252 11.127 14.252H4.37305C2.92338 14.252 1.74818 13.0766 1.74805 11.627V4.87305C1.74805 3.4233 2.9233 2.24805 4.37305 2.24805H4.81738L4.94336 2.26074ZM13.127 1.74805C13.7482 1.74809 14.252 2.25176 14.252 2.87305V8.04199C14.2518 8.66317 13.7482 9.16694 13.127 9.16699C12.5058 9.16693 12.0021 8.66316 12.002 8.04199V5.58887L7.25488 10.3359L7.16895 10.4141C6.7271 10.774 6.07483 10.7476 5.66309 10.3359C5.22426 9.89664 5.22413 9.18433 5.66309 8.74512L10.4102 3.99805H7.95703C7.33606 3.99774 6.83216 3.49406 6.83203 2.87305C6.83203 2.25192 7.33597 1.74836 7.95703 1.74805H13.127Z"
15
15
+
fill="currentColor"
16
16
+
/>
17
17
+
</svg>
18
18
+
);
19
19
+
};
+11
-10
components/PageHeader.tsx
···
3
3
4
4
export const Header = (props: {
5
5
children: React.ReactNode;
6
6
-
hasBackgroundImage: boolean;
6
6
+
cardBorderHidden: boolean;
7
7
}) => {
8
8
let [scrollPos, setScrollPos] = useState(0);
9
9
+
console.log(props.cardBorderHidden);
9
10
10
11
useEffect(() => {
11
12
const homeContent = document.getElementById("home-content");
···
22
23
}
23
24
}, []);
24
25
25
25
-
let headerBGColor = props.hasBackgroundImage
26
26
-
? "var(--bg-page)"
27
27
-
: "var(--bg-leaflet)";
26
26
+
let headerBGColor = props.cardBorderHidden
27
27
+
? "var(--bg-leaflet)"
28
28
+
: "var(--bg-page)";
28
29
29
30
return (
30
31
<div
···
55
56
scrollPos < 20
56
57
? {
57
58
backgroundColor: `rgba(${headerBGColor}, ${scrollPos / 60 + 0.75})`,
58
58
-
paddingLeft: props.hasBackgroundImage
59
59
-
? "8px"
60
60
-
: `calc(${scrollPos / 20}*8px)`,
61
61
-
paddingRight: props.hasBackgroundImage
62
62
-
? "8px"
63
63
-
: `calc(${scrollPos / 20}*8px)`,
59
59
+
paddingLeft: props.cardBorderHidden
60
60
+
? `calc(${scrollPos / 20}*8px)`
61
61
+
: "8px",
62
62
+
paddingRight: props.cardBorderHidden
63
63
+
? `calc(${scrollPos / 20}*8px)`
64
64
+
: "8px",
64
65
}
65
66
: {
66
67
backgroundColor: `rgb(${headerBGColor})`,
+43
-14
components/PageLayouts/DashboardLayout.tsx
···
20
20
import { SearchTiny } from "components/Icons/SearchTiny";
21
21
import { InterfaceState, useIdentityData } from "components/IdentityProvider";
22
22
import { updateIdentityInterfaceState } from "actions/updateIdentityInterfaceState";
23
23
+
import Link from "next/link";
24
24
+
import { ExternalLinkTiny } from "components/Icons/ExternalLinkTiny";
23
25
24
26
export type DashboardState = {
25
27
display?: "grid" | "list";
···
117
119
118
120
export function DashboardLayout<
119
121
T extends {
120
120
-
[name: string]: { content: React.ReactNode; controls: React.ReactNode };
122
122
+
[name: string]: {
123
123
+
content: React.ReactNode;
124
124
+
controls: React.ReactNode;
125
125
+
href?: string;
126
126
+
};
121
127
},
122
128
>(props: {
123
129
id: string;
124
124
-
hasBackgroundImage: boolean;
130
130
+
cardBorderHidden: boolean;
125
131
tabs: T;
126
132
defaultTab: keyof T;
127
133
currentPage: navPages;
···
129
135
actions: React.ReactNode;
130
136
}) {
131
137
let [tab, setTab] = useState(props.defaultTab);
132
132
-
let { content, controls } = props.tabs[tab];
138
138
+
let { content, controls, href } = props.tabs[tab];
133
139
134
140
let [headerState, setHeaderState] = useState<"default" | "controls">(
135
141
"default",
···
154
160
>
155
161
{Object.keys(props.tabs).length <= 1 && !controls ? null : (
156
162
<>
157
157
-
<Header hasBackgroundImage={props.hasBackgroundImage}>
163
163
+
<Header cardBorderHidden={props.cardBorderHidden}>
158
164
{headerState === "default" ? (
159
165
<>
160
166
{Object.keys(props.tabs).length > 1 && (
161
167
<div className="pubDashTabs flex flex-row gap-1">
162
162
-
{Object.keys(props.tabs).map((t) => (
163
163
-
<Tab
164
164
-
key={t}
165
165
-
name={t}
166
166
-
selected={t === tab}
167
167
-
onSelect={() => setTab(t)}
168
168
-
/>
169
169
-
))}
168
168
+
{Object.keys(props.tabs).map((t) => {
169
169
+
if (props.tabs[t].href)
170
170
+
return (
171
171
+
<Link
172
172
+
key={t}
173
173
+
href={props.tabs[t].href}
174
174
+
className="no-underline"
175
175
+
>
176
176
+
<Tab
177
177
+
name={t}
178
178
+
selected={t === tab}
179
179
+
href={props.tabs[t].href}
180
180
+
onSelect={() => setTab(t)}
181
181
+
/>
182
182
+
</Link>
183
183
+
);
184
184
+
return (
185
185
+
<Tab
186
186
+
key={t}
187
187
+
name={t}
188
188
+
selected={t === tab}
189
189
+
onSelect={() => setTab(t)}
190
190
+
/>
191
191
+
);
192
192
+
})}
170
193
</div>
171
194
)}
172
195
{props.publication && (
···
326
349
);
327
350
};
328
351
329
329
-
function Tab(props: { name: string; selected: boolean; onSelect: () => void }) {
352
352
+
function Tab(props: {
353
353
+
name: string;
354
354
+
selected: boolean;
355
355
+
onSelect: () => void;
356
356
+
href?: string;
357
357
+
}) {
330
358
return (
331
359
<div
332
332
-
className={`pubTabs px-1 py-0 rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`}
360
360
+
className={`pubTabs px-1 py-0 flex gap-1 items-center rounded-md hover:cursor-pointer ${props.selected ? "text-accent-2 bg-accent-1 font-bold -mb-px" : "text-tertiary"}`}
333
361
onClick={() => props.onSelect()}
334
362
>
335
363
{props.name}
364
364
+
{props.href && <ExternalLinkTiny />}
336
365
</div>
337
366
);
338
367
}
+1
-1
feeds/index.ts
···
33
33
34
34
let { data: publications } = await supabaseServerClient
35
35
.from("publication_subscriptions")
36
36
-
.select(`publications(documents_in_publications(documents(*)))`)
36
36
+
.select(`publications(*, documents_in_publications(documents(*)))`)
37
37
.eq("identity", auth);
38
38
39
39
const allPosts = (publications || [])