a tool for shared writing and social publishing
at feature/page-blocks 198 lines 6.4 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 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};