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