a tool for shared writing and social publishing
at feature/notifications 194 lines 6.3 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"; 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};