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