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
added capability to backdate post on published docs
cozylittle.house
2 months ago
04eb466e
96ed3674
+214
-32
5 changed files
expand all
collapse all
unified
split
actions
backdatePost.ts
components
Blocks
DateTimeBlock.tsx
DatePicker.tsx
Pages
Backdater.tsx
PublicationMetadata.tsx
+86
actions/backdatePost.ts
···
1
1
+
"use server";
2
2
+
3
3
+
import { AtpBaseClient, PubLeafletDocument } from "lexicons/api";
4
4
+
import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth";
5
5
+
import { getIdentityData } from "actions/getIdentityData";
6
6
+
import { supabaseServerClient } from "supabase/serverClient";
7
7
+
import { Json } from "supabase/database.types";
8
8
+
import { AtUri } from "@atproto/syntax";
9
9
+
10
10
+
type BackdateResult =
11
11
+
| { success: true; publishedAt: string }
12
12
+
| { success: false; error?: OAuthSessionError | string };
13
13
+
14
14
+
export async function backdatePost({
15
15
+
uri,
16
16
+
publishedAt,
17
17
+
}: {
18
18
+
uri: string;
19
19
+
publishedAt: string;
20
20
+
}): Promise<BackdateResult> {
21
21
+
let identity = await getIdentityData();
22
22
+
if (!identity || !identity.atp_did) {
23
23
+
return {
24
24
+
success: false,
25
25
+
error: "Not authenticated",
26
26
+
};
27
27
+
}
28
28
+
29
29
+
const sessionResult = await restoreOAuthSession(identity.atp_did);
30
30
+
if (!sessionResult.ok) {
31
31
+
return { success: false, error: sessionResult.error };
32
32
+
}
33
33
+
let credentialSession = sessionResult.value;
34
34
+
let agent = new AtpBaseClient(
35
35
+
credentialSession.fetchHandler.bind(credentialSession),
36
36
+
);
37
37
+
38
38
+
// Get the existing document
39
39
+
let { data: existingDoc } = await supabaseServerClient
40
40
+
.from("documents")
41
41
+
.select("*")
42
42
+
.eq("uri", uri)
43
43
+
.single();
44
44
+
45
45
+
if (!existingDoc) {
46
46
+
return { success: false, error: "Document not found" };
47
47
+
}
48
48
+
49
49
+
let record = existingDoc.data as PubLeafletDocument.Record;
50
50
+
51
51
+
// Check if the user is the author
52
52
+
if (record.author !== identity.atp_did) {
53
53
+
return { success: false, error: "Not authorized" };
54
54
+
}
55
55
+
56
56
+
let aturi = new AtUri(uri);
57
57
+
58
58
+
// Update the record with the new publishedAt date
59
59
+
let updatedRecord: PubLeafletDocument.Record = {
60
60
+
...record,
61
61
+
publishedAt,
62
62
+
};
63
63
+
64
64
+
// Update the record on ATP
65
65
+
let result = await agent.com.atproto.repo.putRecord({
66
66
+
repo: credentialSession.did!,
67
67
+
rkey: aturi.rkey,
68
68
+
record: updatedRecord,
69
69
+
collection: record.$type,
70
70
+
validate: false,
71
71
+
});
72
72
+
73
73
+
// Optimistically write to our db
74
74
+
let { error } = await supabaseServerClient
75
75
+
.from("documents")
76
76
+
.update({
77
77
+
data: updatedRecord as Json,
78
78
+
})
79
79
+
.eq("uri", uri);
80
80
+
81
81
+
if (error) {
82
82
+
return { success: false, error: error.message };
83
83
+
}
84
84
+
85
85
+
return { success: true, publishedAt };
86
86
+
}
+1
-31
components/Blocks/DateTimeBlock.tsx
···
1
1
import { useEntity, useReplicache } from "src/replicache";
2
2
import { BlockProps, BlockLayout } from "./Block";
3
3
-
import { ChevronProps, DayPicker } from "react-day-picker";
4
3
import { Popover } from "components/Popover";
5
4
import { useEffect, useMemo, useState } from "react";
6
5
import { useEntitySetContext } from "components/EntitySetProvider";
···
10
9
import { Checkbox } from "components/Checkbox";
11
10
import { useHasPageLoaded } from "components/InitialPageLoadProvider";
12
11
import { useSpring, animated } from "@react-spring/web";
13
13
-
import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
14
12
import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall";
13
13
+
import { DayPicker } from "components/DatePicker";
15
14
16
15
export function DateTimeBlock(props: BlockProps) {
17
16
const [isClient, setIsClient] = useState(false);
···
168
167
>
169
168
<div className="flex flex-col gap-3 ">
170
169
<DayPicker
171
171
-
components={{
172
172
-
Chevron: (props: ChevronProps) => <CustomChevron {...props} />,
173
173
-
}}
174
174
-
classNames={{
175
175
-
months: "relative",
176
176
-
month_caption:
177
177
-
"font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md",
178
178
-
button_next:
179
179
-
"absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center",
180
180
-
button_previous:
181
181
-
"absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center ",
182
182
-
chevron: "text-inherit",
183
183
-
month_grid: "w-full table-fixed",
184
184
-
weekdays: "text-secondary text-sm",
185
185
-
selected: "bg-accent-1! text-accent-2 rounded-md font-bold",
186
186
-
187
187
-
day: "h-[34px] text-center rounded-md sm:hover:bg-border-light",
188
188
-
outside: "text-border",
189
189
-
today: "font-bold",
190
190
-
}}
191
191
-
mode="single"
192
170
selected={dateFact ? selectedDate : undefined}
193
171
onSelect={handleDaySelect}
194
172
/>
···
230
208
let spring = useSpring({ opacity: props.active ? 1 : 0 });
231
209
return <animated.div style={spring}>{props.children}</animated.div>;
232
210
};
233
233
-
234
234
-
const CustomChevron = (props: ChevronProps) => {
235
235
-
return (
236
236
-
<div {...props} className="w-full pointer-events-none">
237
237
-
<ArrowRightTiny />
238
238
-
</div>
239
239
-
);
240
240
-
};
+54
components/DatePicker.tsx
···
1
1
+
import { ChevronProps, DayPicker as ReactDayPicker } from "react-day-picker";
2
2
+
import { ArrowRightTiny } from "components/Icons/ArrowRightTiny";
3
3
+
4
4
+
const CustomChevron = (props: ChevronProps) => {
5
5
+
return (
6
6
+
<div {...props} className="w-full pointer-events-none">
7
7
+
<ArrowRightTiny />
8
8
+
</div>
9
9
+
);
10
10
+
};
11
11
+
12
12
+
interface DayPickerProps {
13
13
+
selected: Date | undefined;
14
14
+
onSelect: (date: Date | undefined) => void;
15
15
+
disabled?: (date: Date) => boolean;
16
16
+
toDate?: Date;
17
17
+
}
18
18
+
19
19
+
export const DayPicker = ({
20
20
+
selected,
21
21
+
onSelect,
22
22
+
disabled,
23
23
+
toDate,
24
24
+
}: DayPickerProps) => {
25
25
+
return (
26
26
+
<ReactDayPicker
27
27
+
components={{
28
28
+
Chevron: (props: ChevronProps) => <CustomChevron {...props} />,
29
29
+
}}
30
30
+
classNames={{
31
31
+
months: "relative",
32
32
+
month_caption:
33
33
+
"font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md",
34
34
+
button_next:
35
35
+
"absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center",
36
36
+
button_previous:
37
37
+
"absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center",
38
38
+
chevron: "text-inherit",
39
39
+
month_grid: "w-full table-fixed",
40
40
+
weekdays: "text-secondary text-sm",
41
41
+
selected: "bg-accent-1! text-accent-2 rounded-md font-bold",
42
42
+
day: "h-[34px] text-center rounded-md sm:hover:bg-border-light",
43
43
+
outside: "text-tertiary",
44
44
+
today: "font-bold",
45
45
+
disabled: "text-border cursor-not-allowed hover:bg-transparent!",
46
46
+
}}
47
47
+
mode="single"
48
48
+
selected={selected}
49
49
+
onSelect={onSelect}
50
50
+
disabled={disabled}
51
51
+
toDate={toDate}
52
52
+
/>
53
53
+
);
54
54
+
};
+69
components/Pages/Backdater.tsx
···
1
1
+
"use client";
2
2
+
import { DayPicker } from "components/DatePicker";
3
3
+
import { backdatePost } from "actions/backdatePost";
4
4
+
import { mutate } from "swr";
5
5
+
import { DotLoader } from "components/utils/DotLoader";
6
6
+
import { useToaster } from "components/Toast";
7
7
+
import { useLeafletPublicationData } from "components/PageSWRDataProvider";
8
8
+
import { useState } from "react";
9
9
+
import { timeAgo } from "src/utils/timeAgo";
10
10
+
import { Popover } from "components/Popover";
11
11
+
12
12
+
export const Backdater = (props: { publishedAt: string }) => {
13
13
+
let { data: pub } = useLeafletPublicationData();
14
14
+
let [isUpdating, setIsUpdating] = useState(false);
15
15
+
let [localPublishedAt, setLocalPublishedAt] = useState(props.publishedAt);
16
16
+
let toaster = useToaster();
17
17
+
18
18
+
const handleDaySelect = async (date: Date | undefined) => {
19
19
+
if (!date || !pub?.doc || isUpdating) return;
20
20
+
21
21
+
// Prevent future dates
22
22
+
if (date > new Date()) return;
23
23
+
24
24
+
setIsUpdating(true);
25
25
+
try {
26
26
+
const result = await backdatePost({
27
27
+
uri: pub.doc,
28
28
+
publishedAt: date.toISOString(),
29
29
+
});
30
30
+
31
31
+
if (result.success) {
32
32
+
// Update local state immediately
33
33
+
setLocalPublishedAt(date.toISOString());
34
34
+
// Refresh the publication data
35
35
+
await mutate(`/api/pub/${pub.doc}`);
36
36
+
}
37
37
+
} catch (error) {
38
38
+
console.error("Failed to backdate document:", error);
39
39
+
} finally {
40
40
+
toaster({
41
41
+
content: <div className="font-bold">Updated publish date!</div>,
42
42
+
type: "success",
43
43
+
});
44
44
+
setIsUpdating(false);
45
45
+
}
46
46
+
};
47
47
+
48
48
+
const selectedDate = new Date(localPublishedAt);
49
49
+
50
50
+
return (
51
51
+
<Popover
52
52
+
className="w-64 z-10 px-2!"
53
53
+
trigger={
54
54
+
isUpdating ? (
55
55
+
<DotLoader className="h-[21px]!" />
56
56
+
) : (
57
57
+
<div className="underline">{timeAgo(localPublishedAt)}</div>
58
58
+
)
59
59
+
}
60
60
+
>
61
61
+
<DayPicker
62
62
+
selected={selectedDate}
63
63
+
onSelect={handleDaySelect}
64
64
+
disabled={(date) => date > new Date()}
65
65
+
toDate={new Date()}
66
66
+
/>
67
67
+
</Popover>
68
68
+
);
69
69
+
};
+4
-1
components/Pages/PublicationMetadata.tsx
···
20
20
import { TagSelector } from "components/Tags";
21
21
import { useIdentityData } from "components/IdentityProvider";
22
22
import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader";
23
23
+
import { Backdater } from "./Backdater";
24
24
+
23
25
export const PublicationMetadata = () => {
24
26
let { rep } = useReplicache();
25
27
let { data: pub } = useLeafletPublicationData();
···
96
98
{pub.doc ? (
97
99
<div className="flex gap-2 items-center">
98
100
<p className="text-sm text-tertiary">
99
99
-
Published {publishedAt && timeAgo(publishedAt)}
101
101
+
Published{" "}
102
102
+
{publishedAt && <Backdater publishedAt={publishedAt} />}
100
103
</p>
101
104
102
105
<Link