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
adjusted popover to new one step design
cozylittle.house
4 months ago
3d5ceb23
3cbc54ae
+200
-93
5 changed files
expand all
collapse all
unified
split
app
[leaflet_id]
actions
PublishButton.tsx
login
LoginForm.tsx
components
ActionBar
Publications.tsx
Buttons.tsx
Input.tsx
+154
-65
app/[leaflet_id]/actions/PublishButton.tsx
···
5
5
PubIcon,
6
6
PubListEmptyContent,
7
7
} from "components/ActionBar/Publications";
8
8
+
import { ButtonPrimary, ButtonTertiary } from "components/Buttons";
9
9
+
import { AddSmall } from "components/Icons/AddSmall";
8
10
import { LooseLeafSmall } from "components/Icons/ArchiveSmall";
9
11
import { PublishSmall } from "components/Icons/PublishSmall";
10
12
import { useIdentityData } from "components/IdentityProvider";
13
13
+
import { InputWithLabel } from "components/Input";
11
14
import { Menu, MenuItem } from "components/Layout";
12
15
import { useLeafletPublicationData } from "components/PageSWRDataProvider";
13
16
import { Popover } from "components/Popover";
···
19
22
import { useState } from "react";
20
23
import { useIsMobile } from "src/hooks/isMobile";
21
24
import { useReplicache } from "src/replicache";
25
25
+
import { Json } from "supabase/database.types";
22
26
23
27
export const PublishButton = () => {
24
28
let { data: pub } = useLeafletPublicationData();
···
90
94
let hasPubs =
91
95
identity && identity.atp_did && identity.publications.length > 0;
92
96
93
93
-
if (!hasPubs)
94
94
-
return (
95
95
-
<Menu
96
96
-
asChild
97
97
-
side={isMobile ? "top" : "right"}
98
98
-
align={isMobile ? "center" : "start"}
99
99
-
className="flex flex-col max-w-xs text-secondary"
100
100
-
trigger={
101
101
-
<ActionButton
102
102
-
primary
103
103
-
icon={<PublishSmall className="shrink-0" />}
104
104
-
label={"Publish on ATP"}
105
105
-
/>
106
106
-
}
107
107
-
>
108
108
-
<div className="text-sm text-tertiary">Publish to…</div>
109
109
-
{identity?.publications?.map((d) => {
110
110
-
return (
111
111
-
<MenuItem
112
112
-
onSelect={async () => {
113
113
-
// TODO
114
114
-
// make this a draft of the selected Publication
115
115
-
// redirect to the publication publish page
116
116
-
}}
117
117
-
>
118
118
-
<PubIcon
119
119
-
record={d.record as PubLeafletPublication.Record}
120
120
-
uri={d.uri}
121
121
-
/>
122
122
-
<div className=" w-full truncate font-bold">{d.name}</div>
123
123
-
</MenuItem>
124
124
-
);
125
125
-
})}
126
126
-
<hr className="border-border-light my-1" />
127
127
-
<MenuItem
128
128
-
onSelect={() => {
129
129
-
// TODO
130
130
-
// send to one-off /publish page
131
131
-
}}
132
132
-
>
133
133
-
<LooseLeafSmall />
134
134
-
<div className="font-bold pb-1">Publish as One-Off</div>
135
135
-
</MenuItem>
136
136
-
</Menu>
137
137
-
);
138
138
-
else
139
139
-
return (
140
140
-
<Popover
141
141
-
asChild
142
142
-
side={isMobile ? "top" : "right"}
143
143
-
align={isMobile ? "center" : "start"}
144
144
-
className="p-1!"
145
145
-
trigger={
146
146
-
<ActionButton
147
147
-
primary
148
148
-
icon={<PublishSmall className="shrink-0" />}
149
149
-
label={"Publish on ATP"}
150
150
-
/>
151
151
-
}
152
152
-
>
153
153
-
{/* this component is also used on Home to populate the sidebar when PubList is empty */}
154
154
-
{/* however, this component needs to redirect to sign in, pub creation, AND publish so we might need to just make a new component */}
97
97
+
return (
98
98
+
<Popover
99
99
+
asChild
100
100
+
side={isMobile ? "top" : "right"}
101
101
+
align={isMobile ? "center" : "start"}
102
102
+
className="max-w-xs w-[1000px]"
103
103
+
trigger={
104
104
+
<ActionButton
105
105
+
primary
106
106
+
icon={<PublishSmall className="shrink-0" />}
107
107
+
label={"Publish on ATP"}
108
108
+
/>
109
109
+
}
110
110
+
>
111
111
+
{!identity || !identity.atp_did ? (
112
112
+
// this component is also used on Home to populate the sidebar when PubList is empty
113
113
+
// when user doesn't have an AT Proto account, and redirects back to the doc (hopefully with publish open?
114
114
+
<div className="-mx-2 -my-1">
115
115
+
<PubListEmptyContent compact />
116
116
+
</div>
117
117
+
) : (
118
118
+
<div className="flex flex-col">
119
119
+
<PostDetailsForm />
120
120
+
<hr className="border-border-light my-3" />
121
121
+
<div>
122
122
+
<PubSelector publications={identity.publications} />
123
123
+
</div>
124
124
+
<hr className="border-border-light mt-3 mb-2" />
125
125
+
126
126
+
<div className="flex gap-2 items-center place-self-end">
127
127
+
<ButtonTertiary>Save as Draft</ButtonTertiary>
128
128
+
<ButtonPrimary>Next</ButtonPrimary>
129
129
+
</div>
130
130
+
</div>
131
131
+
)}
132
132
+
</Popover>
133
133
+
);
134
134
+
};
135
135
+
136
136
+
const PostDetailsForm = () => {
137
137
+
let [description, setDescription] = useState("");
138
138
+
139
139
+
return (
140
140
+
<div className=" flex flex-col gap-1">
141
141
+
<div className="text-sm text-tertiary">Post Details</div>
142
142
+
<div className="flex flex-col gap-2">
143
143
+
<InputWithLabel label="Title" value={"Title goes here"} disabled />
144
144
+
<InputWithLabel
145
145
+
label="Description (optional)"
146
146
+
textarea
147
147
+
value={description}
148
148
+
className="h-[4lh]"
149
149
+
onChange={(e) => setDescription(e.currentTarget.value)}
150
150
+
/>
151
151
+
</div>
152
152
+
</div>
153
153
+
);
154
154
+
};
155
155
+
156
156
+
const PubSelector = (props: {
157
157
+
publications: {
158
158
+
identity_did: string;
159
159
+
indexed_at: string;
160
160
+
name: string;
161
161
+
record: Json | null;
162
162
+
uri: string;
163
163
+
}[];
164
164
+
}) => {
165
165
+
let [selectedPub, setSelectedPub] = useState<string | undefined>(undefined);
166
166
+
return (
167
167
+
<div className="flex flex-col gap-1">
168
168
+
<div className="text-sm text-tertiary">Publish to…</div>
169
169
+
{props.publications.length === 0 || props.publications === undefined ? (
170
170
+
<div className="flex flex-col gap-3">
171
171
+
<div className="flex gap-2 menuItem">
172
172
+
<LooseLeafSmall className="shrink-0" />
173
173
+
<div className="flex flex-col leading-snug">
174
174
+
<div className="text-secondary font-bold">
175
175
+
Publish as LooseLeaf
176
176
+
</div>
177
177
+
<div className="text-tertiary text-sm">
178
178
+
Publish this as a one off doc <br />
179
179
+
to AT Proto
180
180
+
</div>
181
181
+
</div>
182
182
+
</div>
183
183
+
<div className="flex gap-2 menuItem">
184
184
+
<PublishSmall className="shrink-0" />
185
185
+
<div className="flex flex-col leading-snug">
186
186
+
<div className="text-secondary font-bold">
187
187
+
Start a Publication!
188
188
+
</div>
189
189
+
<div className="text-tertiary text-sm">
190
190
+
Publish your writing to a blog or newsletter on AT Proto
191
191
+
</div>
192
192
+
</div>
193
193
+
</div>
194
194
+
</div>
195
195
+
) : (
196
196
+
<div className="flex flex-col gap-3">
197
197
+
<PubOption
198
198
+
selected={selectedPub === "looseleaf"}
199
199
+
onSelect={() => setSelectedPub("looseleaf")}
200
200
+
>
201
201
+
<LooseLeafSmall />
202
202
+
Publish as Looseleaf
203
203
+
</PubOption>
204
204
+
<hr className="border-border-light border-dashed " />
205
205
+
{props.publications.map((p) => {
206
206
+
let pubRecord = p.record as PubLeafletPublication.Record;
207
207
+
return (
208
208
+
<PubOption
209
209
+
selected={selectedPub === p.uri}
210
210
+
onSelect={() => setSelectedPub(p.uri)}
211
211
+
>
212
212
+
<>
213
213
+
<PubIcon record={pubRecord} uri={p.uri} />
214
214
+
{p.name}
215
215
+
</>
216
216
+
</PubOption>
217
217
+
);
218
218
+
})}
219
219
+
<PubOption
220
220
+
selected={selectedPub === "create"}
221
221
+
onSelect={() => setSelectedPub("create")}
222
222
+
>
223
223
+
<>
224
224
+
<AddSmall /> Create New Publication
225
225
+
</>
226
226
+
</PubOption>
227
227
+
</div>
228
228
+
)}
229
229
+
</div>
230
230
+
);
231
231
+
};
155
232
156
156
-
<PubListEmptyContent />
157
157
-
</Popover>
158
158
-
);
233
233
+
const PubOption = (props: {
234
234
+
selected: boolean;
235
235
+
onSelect: () => void;
236
236
+
children: React.ReactNode;
237
237
+
}) => {
238
238
+
return (
239
239
+
<button
240
240
+
className={`flex gap-2 menuItem font-bold text-secondary ${props.selected && "bg-test"}`}
241
241
+
onClick={() => {
242
242
+
props.onSelect();
243
243
+
}}
244
244
+
>
245
245
+
{props.children}
246
246
+
</button>
247
247
+
);
159
248
};
+1
-1
app/login/LoginForm.tsx
···
213
213
</ButtonPrimary>
214
214
<button
215
215
type="button"
216
216
-
className={`${props.compact ? "text-xs" : "text-sm"} text-accent-contrast place-self-center mt-[6px]`}
216
216
+
className={`${props.compact ? "text-xs mt-0.5" : "text-sm mt-[6px]"} text-accent-contrast place-self-center`}
217
217
onClick={() => setSigningWithHandle(true)}
218
218
>
219
219
use an ATProto handle
+6
-4
components/ActionBar/Publications.tsx
···
118
118
);
119
119
};
120
120
121
121
-
export const PubListEmptyContent = () => {
121
121
+
export const PubListEmptyContent = (props: { compact?: boolean }) => {
122
122
let { identity } = useIdentityData();
123
123
124
124
return (
125
125
-
<div className="bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm">
125
125
+
<div
126
126
+
className={`bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm`}
127
127
+
>
126
128
<div className="mx-auto pt-2 scale-90">
127
129
<PubListEmptyIllo />
128
130
</div>
129
131
<div className="pt-1 font-bold">Publish on AT Proto</div>
130
130
-
{identity && identity.atp_did ? (
132
132
+
{identity && !identity.atp_did ? (
131
133
// has ATProto account and no pubs
132
134
<>
133
135
<div className="pb-2 text-secondary text-xs">
···
144
146
// no ATProto account and no pubs
145
147
<>
146
148
<div className="pb-2 text-secondary text-xs">
147
147
-
Link a Bluesky account to start a new publication on AT Proto
149
149
+
Link a Bluesky account to start <br /> a new publication on AT Proto
148
150
</div>
149
151
150
152
<BlueskyLogin compact />
+35
-21
components/Buttons.tsx
···
10
10
import { PopoverArrow } from "./Icons/PopoverArrow";
11
11
12
12
type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">;
13
13
+
13
14
export const ButtonPrimary = forwardRef<
14
15
HTMLButtonElement,
15
16
ButtonProps & {
···
35
36
m-0 h-max
36
37
${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"}
37
38
${compact ? "py-0 px-1" : "px-2 py-0.5 "}
38
38
-
bg-accent-1 outline-transparent border border-accent-1
39
39
-
rounded-md text-base font-bold text-accent-2
39
39
+
bg-accent-1 disabled:bg-border-light
40
40
+
border border-accent-1 rounded-md disabled:border-border-light
41
41
+
outline outline-transparent outline-offset-1 focus:outline-accent-1 hover:outline-accent-1
42
42
+
text-base font-bold text-accent-2 disabled:text-border disabled:hover:text-border
40
43
flex gap-2 items-center justify-center shrink-0
41
41
-
transparent-outline focus:outline-accent-1 hover:outline-accent-1 outline-offset-1
42
42
-
disabled:bg-border-light disabled:border-border-light disabled:text-border disabled:hover:text-border
43
44
${className}
44
45
`}
45
46
>
···
70
71
<button
71
72
{...buttonProps}
72
73
ref={ref}
73
73
-
className={`m-0 h-max
74
74
+
className={`
75
75
+
m-0 h-max
74
76
${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"}
75
75
-
${props.compact ? "py-0 px-1" : "px-2 py-0.5 "}
76
76
-
bg-bg-page outline-transparent
77
77
-
rounded-md text-base font-bold text-accent-contrast
78
78
-
flex gap-2 items-center justify-center shrink-0
79
79
-
transparent-outline focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1
80
80
-
border border-accent-contrast
81
81
-
disabled:bg-border-light disabled:text-border disabled:hover:text-border
82
82
-
${props.className}
83
83
-
`}
77
77
+
${compact ? "py-0 px-1" : "px-2 py-0.5 "}
78
78
+
bg-bg-page disabled:bg-border-light
79
79
+
border border-accent-contrast rounded-md
80
80
+
outline outline-transparent focus:outline-accent-contrast hover:outline-accent-contrast outline-offset-1
81
81
+
text-base font-bold text-accent-contrast disabled:text-border disabled:hover:text-border
82
82
+
flex gap-2 items-center justify-center shrink-0
83
83
+
${props.className}
84
84
+
`}
84
85
>
85
86
{props.children}
86
87
</button>
···
92
93
HTMLButtonElement,
93
94
{
94
95
fullWidth?: boolean;
96
96
+
fullWidthOnMobile?: boolean;
95
97
children: React.ReactNode;
96
98
compact?: boolean;
97
99
} & ButtonProps
98
100
>((props, ref) => {
99
99
-
let { fullWidth, children, compact, ...buttonProps } = props;
101
101
+
let {
102
102
+
className,
103
103
+
fullWidth,
104
104
+
fullWidthOnMobile,
105
105
+
compact,
106
106
+
children,
107
107
+
...buttonProps
108
108
+
} = props;
100
109
return (
101
110
<button
102
111
{...buttonProps}
103
112
ref={ref}
104
104
-
className={`m-0 h-max ${fullWidth ? "w-full" : "w-max"} ${compact ? "px-0" : "px-1"}
105
105
-
bg-transparent text-base font-bold text-accent-contrast
106
106
-
flex gap-2 items-center justify-center shrink-0
107
107
-
hover:underline disabled:text-border
108
108
-
${props.className}
109
109
-
`}
113
113
+
className={`
114
114
+
m-0 h-max
115
115
+
${fullWidth ? "w-full" : fullWidthOnMobile ? "w-full sm:w-max" : "w-max"}
116
116
+
${compact ? "py-0 px-1" : "px-2 py-0.5 "}
117
117
+
bg-transparent hover:bg-[var(--accent-light)]
118
118
+
border border-transparent rounded-md hover:border-[var(--accent-light)]
119
119
+
outline outline-transparent focus:outline-[var(--accent-light)] hover:outline-[var(--accent-light)] outline-offset-1
120
120
+
text-base font-bold text-accent-contrast disabled:text-border
121
121
+
flex gap-2 items-center justify-center shrink-0
122
122
+
${props.className}
123
123
+
`}
110
124
>
111
125
{children}
112
126
</button>
+4
-2
components/Input.tsx
···
100
100
JSX.IntrinsicElements["textarea"],
101
101
) => {
102
102
let { label, textarea, ...inputProps } = props;
103
103
-
let style = `appearance-none w-full font-normal not-italic bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`;
103
103
+
let style = `appearance-none w-full font-normal not-italic bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none disabled:text-tertiary disabled:cursor-not-allowed`;
104
104
return (
105
105
-
<label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!">
105
105
+
<label
106
106
+
className={`input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]! ${props.disabled && "bg-border-light! cursor-not-allowed!"}`}
107
107
+
>
106
108
{props.label}
107
109
{textarea ? (
108
110
<textarea {...inputProps} className={style} />