forked from
leaflet.pub/leaflet
a tool for shared writing and social publishing
1import Link from "next/link";
2import { useLeafletPublicationData } from "components/PageSWRDataProvider";
3import { useRef } from "react";
4import { useReplicache } from "src/replicache";
5import { AsyncValueAutosizeTextarea } from "components/utils/AutosizeTextarea";
6import { Separator } from "components/Layout";
7import { AtUri } from "@atproto/syntax";
8import { PubLeafletDocument } from "lexicons/api";
9import {
10 getBasePublicationURL,
11 getPublicationURL,
12} from "app/lish/createPub/getPublicationURL";
13import { useSubscribe } from "src/replicache/useSubscribe";
14import { useEntitySetContext } from "components/EntitySetProvider";
15import { timeAgo } from "src/utils/timeAgo";
16import { useIdentityData } from "components/IdentityProvider";
17export const PublicationMetadata = () => {
18 let { rep } = useReplicache();
19 let { data: pub } = useLeafletPublicationData();
20 let { identity } = useIdentityData();
21 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));
22 let description = useSubscribe(rep, (tx) =>
23 tx.get<string>("publication_description"),
24 );
25 let record = pub?.documents?.data as PubLeafletDocument.Record | null;
26 let publishedAt = record?.publishedAt;
27
28 if (!pub || !pub.publications) return null;
29
30 if (typeof title !== "string") {
31 title = pub?.title || "";
32 }
33 if (typeof description !== "string") {
34 description = pub?.description || "";
35 }
36 return (
37 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}>
38 <div className="flex gap-2">
39 <Link
40 href={
41 identity?.atp_did === pub.publications?.identity_did
42 ? `${getBasePublicationURL(pub.publications)}/dashboard`
43 : getPublicationURL(pub.publications)
44 }
45 className="leafletMetadata text-accent-contrast font-bold hover:no-underline"
46 >
47 {pub.publications?.name}
48 </Link>
49 <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border-light rounded-md ">
50 Editor
51 </div>
52 </div>
53 <TextField
54 className="text-xl font-bold outline-hidden bg-transparent"
55 value={title}
56 onChange={async (newTitle) => {
57 await rep?.mutate.updatePublicationDraft({
58 title: newTitle,
59 description,
60 });
61 }}
62 placeholder="Untitled"
63 />
64 <TextField
65 placeholder="add an optional description..."
66 className="italic text-secondary outline-hidden bg-transparent"
67 value={description}
68 onChange={async (newDescription) => {
69 await rep?.mutate.updatePublicationDraft({
70 title,
71 description: newDescription,
72 });
73 }}
74 />
75 {pub.doc ? (
76 <div className="flex flex-row items-center gap-2 pt-3">
77 <p className="text-sm text-tertiary">
78 Published {publishedAt && timeAgo(publishedAt)}
79 </p>
80 <Separator classname="h-4" />
81 <Link
82 target="_blank"
83 className="text-sm"
84 href={`${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}`}
85 >
86 View Post
87 </Link>
88 </div>
89 ) : (
90 <p className="text-sm text-tertiary pt-2">Draft</p>
91 )}
92 </div>
93 );
94};
95
96export const TextField = ({
97 value,
98 onChange,
99 className,
100 placeholder,
101}: {
102 value: string;
103 onChange: (v: string) => Promise<void>;
104 className: string;
105 placeholder: string;
106}) => {
107 let { undoManager } = useReplicache();
108 let actionTimeout = useRef<number | null>(null);
109 let { permissions } = useEntitySetContext();
110 let previousSelection = useRef<null | { start: number; end: number }>(null);
111 let ref = useRef<HTMLTextAreaElement | null>(null);
112 return (
113 <AsyncValueAutosizeTextarea
114 ref={ref}
115 disabled={!permissions.write}
116 onSelect={(e) => {
117 let start = e.currentTarget.selectionStart,
118 end = e.currentTarget.selectionEnd;
119 previousSelection.current = { start, end };
120 }}
121 className={className}
122 value={value}
123 onBlur={async () => {
124 if (actionTimeout.current) {
125 undoManager.endGroup();
126 window.clearTimeout(actionTimeout.current);
127 actionTimeout.current = null;
128 }
129 }}
130 onChange={async (e) => {
131 let newValue = e.currentTarget.value;
132 let oldValue = value;
133 let start = e.currentTarget.selectionStart,
134 end = e.currentTarget.selectionEnd;
135 await onChange(e.currentTarget.value);
136
137 if (actionTimeout.current) {
138 window.clearTimeout(actionTimeout.current);
139 } else {
140 undoManager.startGroup();
141 }
142
143 actionTimeout.current = window.setTimeout(() => {
144 undoManager.endGroup();
145 actionTimeout.current = null;
146 }, 200);
147 let previousStart = previousSelection.current?.start || null,
148 previousEnd = previousSelection.current?.end || null;
149 undoManager.add({
150 redo: async () => {
151 await onChange(newValue);
152 ref.current?.setSelectionRange(start, end);
153 ref.current?.focus();
154 },
155 undo: async () => {
156 await onChange(oldValue);
157 ref.current?.setSelectionRange(previousStart, previousEnd);
158 ref.current?.focus();
159 },
160 });
161 }}
162 placeholder={placeholder}
163 />
164 );
165};
166
167export const PublicationMetadataPreview = () => {
168 let { data: pub } = useLeafletPublicationData();
169 let record = pub?.documents?.data as PubLeafletDocument.Record | null;
170 let publishedAt = record?.publishedAt;
171
172 if (!pub || !pub.publications) return null;
173
174 return (
175 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}>
176 <div className="text-accent-contrast font-bold hover:no-underline">
177 {pub.publications?.name}
178 </div>
179
180 <div
181 className={`text-xl font-bold outline-hidden bg-transparent ${!pub.title && "text-tertiary italic"}`}
182 >
183 {pub.title ? pub.title : "Untitled"}
184 </div>
185 <div className="italic text-secondary outline-hidden bg-transparent">
186 {pub.description}
187 </div>
188
189 {pub.doc ? (
190 <div className="flex flex-row items-center gap-2 pt-3">
191 <p className="text-sm text-tertiary">
192 Published {publishedAt && timeAgo(publishedAt)}
193 </p>
194 </div>
195 ) : (
196 <p className="text-sm text-tertiary pt-2">Draft</p>
197 )}
198 </div>
199 );
200};