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