a tool for shared writing and social publishing

Feature/backdate (#258)

* added capability to backdate post on published docs

* componentize Date Picker, add Time Picker, mock up date and time pickers
in publish show and update flow

* wire up backdating posts from the publish flow. still broken from update
button

* wire up updating pubish date on published post

* just bold some stuff

* open calendars to corrent months and times by default

authored by cozylittle.house and committed by GitHub e13306ec 08302114

+297 -52
+7 -4
actions/publishToPublication.ts
··· 66 tags, 67 cover_image, 68 entitiesToDelete, 69 }: { 70 root_entity: string; 71 publication_uri?: string; ··· 75 tags?: string[]; 76 cover_image?: string | null; 77 entitiesToDelete?: string[]; 78 }): Promise<PublishResult> { 79 let identity = await getIdentityData(); 80 if (!identity || !identity.atp_did) { ··· 147 credentialSession.did!, 148 ); 149 150 - let existingRecord = 151 - (draft?.documents?.data as PubLeafletDocument.Record | undefined) || {}; 152 153 // Extract theme for standalone documents (not for publications) 154 let theme: PubLeafletPublication.Theme | undefined; ··· 174 } 175 176 let record: PubLeafletDocument.Record = { 177 - publishedAt: new Date().toISOString(), 178 - ...existingRecord, 179 $type: "pub.leaflet.document", 180 author: credentialSession.did!, 181 ...(publication_uri && { publication: publication_uri }), ··· 199 }; 200 } 201 }), 202 }; 203 204 // Keep the same rkey if updating an existing document
··· 66 tags, 67 cover_image, 68 entitiesToDelete, 69 + publishedAt, 70 }: { 71 root_entity: string; 72 publication_uri?: string; ··· 76 tags?: string[]; 77 cover_image?: string | null; 78 entitiesToDelete?: string[]; 79 + publishedAt?: string; 80 }): Promise<PublishResult> { 81 let identity = await getIdentityData(); 82 if (!identity || !identity.atp_did) { ··· 149 credentialSession.did!, 150 ); 151 152 + let existingRecord = draft?.documents?.data as 153 + | PubLeafletDocument.Record 154 + | undefined; 155 156 // Extract theme for standalone documents (not for publications) 157 let theme: PubLeafletPublication.Theme | undefined; ··· 177 } 178 179 let record: PubLeafletDocument.Record = { 180 $type: "pub.leaflet.document", 181 author: credentialSession.did!, 182 ...(publication_uri && { publication: publication_uri }), ··· 200 }; 201 } 202 }), 203 + publishedAt: 204 + publishedAt || existingRecord?.publishedAt || new Date().toISOString(), 205 }; 206 207 // Keep the same rkey if updating an existing document
+9 -2
app/[leaflet_id]/actions/PublishButton.tsx
··· 24 import { DotLoader } from "components/utils/DotLoader"; 25 import { PubLeafletPublication } from "lexicons/api"; 26 import { useParams, useRouter, useSearchParams } from "next/navigation"; 27 - import { useState, useMemo } from "react"; 28 import { useIsMobile } from "src/hooks/isMobile"; 29 import { useReplicache, useEntity } from "src/replicache"; 30 import { useSubscribe } from "src/replicache/useSubscribe"; ··· 40 import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; 41 import { AddTiny } from "components/Icons/AddTiny"; 42 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 43 44 export const PublishButton = (props: { entityID: string }) => { 45 let { data: pub } = useLeafletPublicationData(); ··· 95 tx.get<string | null>("publication_cover_image"), 96 ); 97 98 return ( 99 <ActionButton 100 primary ··· 111 description: currentDescription, 112 tags: currentTags, 113 cover_image: coverImage, 114 }); 115 setIsLoading(false); 116 mutate(); ··· 134 135 toaster({ 136 content: ( 137 - <div> 138 {pub.doc ? "Updated! " : "Published! "} 139 <SpeedyLink className="underline" href={docUrl}> 140 See Published Post
··· 24 import { DotLoader } from "components/utils/DotLoader"; 25 import { PubLeafletPublication } from "lexicons/api"; 26 import { useParams, useRouter, useSearchParams } from "next/navigation"; 27 + import { useState, useMemo, useEffect } from "react"; 28 import { useIsMobile } from "src/hooks/isMobile"; 29 import { useReplicache, useEntity } from "src/replicache"; 30 import { useSubscribe } from "src/replicache/useSubscribe"; ··· 40 import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; 41 import { AddTiny } from "components/Icons/AddTiny"; 42 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 43 + import { useLocalPublishedAt } from "components/Pages/Backdater"; 44 45 export const PublishButton = (props: { entityID: string }) => { 46 let { data: pub } = useLeafletPublicationData(); ··· 96 tx.get<string | null>("publication_cover_image"), 97 ); 98 99 + // Get local published at from Replicache (session-only state, not persisted to DB) 100 + let publishedAt = useLocalPublishedAt((s) => 101 + pub?.doc ? s[pub?.doc] : undefined, 102 + ); 103 + 104 return ( 105 <ActionButton 106 primary ··· 117 description: currentDescription, 118 tags: currentTags, 119 cover_image: coverImage, 120 + publishedAt: publishedAt?.toISOString(), 121 }); 122 setIsLoading(false); 123 mutate(); ··· 141 142 toaster({ 143 content: ( 144 + <div className="font-bold"> 145 {pub.doc ? "Updated! " : "Published! "} 146 <SpeedyLink className="underline" href={docUrl}> 147 See Published Post
+114 -11
app/[leaflet_id]/publish/PublishPost.tsx
··· 23 import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 24 import { PubIcon } from "components/ActionBar/Publications"; 25 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 26 27 type Props = { 28 title: string; ··· 78 ); 79 let [localTags, setLocalTags] = useState<string[]>([]); 80 81 // Get cover image from Replicache 82 let replicacheCoverImage = useSubscribe(rep, (tx) => 83 tx.get<string | null>("publication_cover_image"), 84 ); 85 86 // Use Replicache tags only when we have a draft 87 - const hasDraft = props.hasDraft; 88 - const currentTags = hasDraft 89 ? Array.isArray(replicacheTags) 90 ? replicacheTags 91 : [] ··· 93 94 // Update tags via Replicache mutation or local state depending on context 95 const handleTagsChange = async (newTags: string[]) => { 96 - if (hasDraft) { 97 await rep?.mutate.updatePublicationDraft({ 98 tags: newTags, 99 }); ··· 116 tags: currentTags, 117 cover_image: replicacheCoverImage, 118 entitiesToDelete: props.entitiesToDelete, 119 }); 120 121 if (!result.success) { ··· 168 record={props.record} 169 /> 170 <hr className="border-border" /> 171 - <ShareOptions 172 - setShareOption={setShareOption} 173 - shareOption={shareOption} 174 - charCount={charCount} 175 - setCharCount={setCharCount} 176 - editorStateRef={editorStateRef} 177 - {...props} 178 /> 179 <hr className="border-border " /> 180 <div className="flex flex-col gap-2"> 181 <h4>Tags</h4> 182 <TagSelector ··· 184 setSelectedTags={handleTagsChange} 185 /> 186 </div> 187 <hr className="border-border mb-2" /> 188 189 <div className="flex flex-col gap-2"> ··· 219 ); 220 }; 221 222 const ShareOptions = (props: { 223 shareOption: "quiet" | "bluesky"; 224 setShareOption: (option: typeof props.shareOption) => void; ··· 232 }) => { 233 return ( 234 <div className="flex flex-col gap-2"> 235 - <h4>Notifications</h4> 236 <Radio 237 checked={props.shareOption === "quiet"} 238 onChange={(e) => {
··· 23 import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 24 import { PubIcon } from "components/ActionBar/Publications"; 25 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 26 + import { DatePicker, TimePicker } from "components/DatePicker"; 27 + import { Popover } from "components/Popover"; 28 + import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 29 + import { Separator } from "react-aria-components"; 30 + import { setHours, setMinutes } from "date-fns"; 31 32 type Props = { 33 title: string; ··· 83 ); 84 let [localTags, setLocalTags] = useState<string[]>([]); 85 86 + let [localPublishedAt, setLocalPublishedAt] = useState<Date | undefined>( 87 + undefined, 88 + ); 89 // Get cover image from Replicache 90 let replicacheCoverImage = useSubscribe(rep, (tx) => 91 tx.get<string | null>("publication_cover_image"), 92 ); 93 94 // Use Replicache tags only when we have a draft 95 + const currentTags = props.hasDraft 96 ? Array.isArray(replicacheTags) 97 ? replicacheTags 98 : [] ··· 100 101 // Update tags via Replicache mutation or local state depending on context 102 const handleTagsChange = async (newTags: string[]) => { 103 + if (props.hasDraft) { 104 await rep?.mutate.updatePublicationDraft({ 105 tags: newTags, 106 }); ··· 123 tags: currentTags, 124 cover_image: replicacheCoverImage, 125 entitiesToDelete: props.entitiesToDelete, 126 + publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 127 }); 128 129 if (!result.success) { ··· 176 record={props.record} 177 /> 178 <hr className="border-border" /> 179 + 180 + <BackdateOptions 181 + publishedAt={localPublishedAt} 182 + setPublishedAt={setLocalPublishedAt} 183 /> 184 <hr className="border-border " /> 185 + 186 <div className="flex flex-col gap-2"> 187 <h4>Tags</h4> 188 <TagSelector ··· 190 setSelectedTags={handleTagsChange} 191 /> 192 </div> 193 + <hr className="border-border" /> 194 + <ShareOptions 195 + setShareOption={setShareOption} 196 + shareOption={shareOption} 197 + charCount={charCount} 198 + setCharCount={setCharCount} 199 + editorStateRef={editorStateRef} 200 + {...props} 201 + /> 202 <hr className="border-border mb-2" /> 203 204 <div className="flex flex-col gap-2"> ··· 234 ); 235 }; 236 237 + const BackdateOptions = (props: { 238 + publishedAt: Date | undefined; 239 + setPublishedAt: (date: Date | undefined) => void; 240 + }) => { 241 + const formattedDate = useLocalizedDate( 242 + props.publishedAt?.toISOString() || "", 243 + { 244 + month: "short", 245 + day: "numeric", 246 + year: "numeric", 247 + hour: "numeric", 248 + minute: "numeric", 249 + hour12: true, 250 + }, 251 + ); 252 + 253 + const [timeValue, setTimeValue] = useState<string>(() => { 254 + const date = props.publishedAt || new Date(); 255 + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; 256 + }); 257 + 258 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 259 + 260 + const handleTimeChange = (time: string) => { 261 + setTimeValue(time); 262 + if (!props.publishedAt) return; 263 + 264 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 265 + const newDate = setHours(setMinutes(props.publishedAt, minutes), hours); 266 + const currentDate = new Date(); 267 + 268 + if (newDate > currentDate) { 269 + props.setPublishedAt(currentDate); 270 + setTimeValue(currentTime); 271 + } else props.setPublishedAt(newDate); 272 + }; 273 + 274 + const handleDateChange = (date: Date | undefined) => { 275 + if (!date) { 276 + props.setPublishedAt(undefined); 277 + return; 278 + } 279 + const [hours, minutes] = timeValue 280 + .split(":") 281 + .map((str) => parseInt(str, 10)); 282 + const newDate = new Date( 283 + date.getFullYear(), 284 + date.getMonth(), 285 + date.getDate(), 286 + hours, 287 + minutes, 288 + ); 289 + const currentDate = new Date(); 290 + if (newDate > currentDate) { 291 + props.setPublishedAt(currentDate); 292 + setTimeValue(currentTime); 293 + } else props.setPublishedAt(newDate); 294 + }; 295 + 296 + return ( 297 + <div className="flex justify-between gap-2"> 298 + <h4>Publish Date</h4> 299 + <Popover 300 + className="w-64 px-2!" 301 + trigger={ 302 + props.publishedAt ? ( 303 + <div className="text-secondary">{formattedDate}</div> 304 + ) : ( 305 + <div className="text-tertiary italic">now</div> 306 + ) 307 + } 308 + > 309 + <div className="flex flex-col gap-3"> 310 + <DatePicker 311 + selected={props.publishedAt} 312 + onSelect={handleDateChange} 313 + disabled={(date) => date > new Date()} 314 + /> 315 + <Separator className="border-border" /> 316 + <div className="flex gap-4 pb-1 items-center"> 317 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 318 + </div> 319 + </div> 320 + </Popover> 321 + </div> 322 + ); 323 + }; 324 + 325 const ShareOptions = (props: { 326 shareOption: "quiet" | "bluesky"; 327 setShareOption: (option: typeof props.shareOption) => void; ··· 335 }) => { 336 return ( 337 <div className="flex flex-col gap-2"> 338 + <h4>Share and Notify</h4> 339 <Radio 340 checked={props.shareOption === "quiet"} 341 onChange={(e) => {
+2 -32
components/Blocks/DateTimeBlock.tsx
··· 1 import { useEntity, useReplicache } from "src/replicache"; 2 import { BlockProps, BlockLayout } from "./Block"; 3 - import { ChevronProps, DayPicker } from "react-day-picker"; 4 import { Popover } from "components/Popover"; 5 import { useEffect, useMemo, useState } from "react"; 6 import { useEntitySetContext } from "components/EntitySetProvider"; ··· 10 import { Checkbox } from "components/Checkbox"; 11 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 12 import { useSpring, animated } from "@react-spring/web"; 13 - import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 14 import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; 15 16 export function DateTimeBlock(props: BlockProps) { 17 const [isClient, setIsClient] = useState(false); ··· 167 } 168 > 169 <div className="flex flex-col gap-3 "> 170 - <DayPicker 171 - components={{ 172 - Chevron: (props: ChevronProps) => <CustomChevron {...props} />, 173 - }} 174 - classNames={{ 175 - months: "relative", 176 - month_caption: 177 - "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", 178 - button_next: 179 - "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", 180 - button_previous: 181 - "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center ", 182 - chevron: "text-inherit", 183 - month_grid: "w-full table-fixed", 184 - weekdays: "text-secondary text-sm", 185 - selected: "bg-accent-1! text-accent-2 rounded-md font-bold", 186 - 187 - day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", 188 - outside: "text-border", 189 - today: "font-bold", 190 - }} 191 - mode="single" 192 selected={dateFact ? selectedDate : undefined} 193 onSelect={handleDaySelect} 194 /> ··· 230 let spring = useSpring({ opacity: props.active ? 1 : 0 }); 231 return <animated.div style={spring}>{props.children}</animated.div>; 232 }; 233 - 234 - const CustomChevron = (props: ChevronProps) => { 235 - return ( 236 - <div {...props} className="w-full pointer-events-none"> 237 - <ArrowRightTiny /> 238 - </div> 239 - ); 240 - };
··· 1 import { useEntity, useReplicache } from "src/replicache"; 2 import { BlockProps, BlockLayout } from "./Block"; 3 import { Popover } from "components/Popover"; 4 import { useEffect, useMemo, useState } from "react"; 5 import { useEntitySetContext } from "components/EntitySetProvider"; ··· 9 import { Checkbox } from "components/Checkbox"; 10 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 11 import { useSpring, animated } from "@react-spring/web"; 12 import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; 13 + import { DatePicker } from "components/DatePicker"; 14 15 export function DateTimeBlock(props: BlockProps) { 16 const [isClient, setIsClient] = useState(false); ··· 166 } 167 > 168 <div className="flex flex-col gap-3 "> 169 + <DatePicker 170 selected={dateFact ? selectedDate : undefined} 171 onSelect={handleDaySelect} 172 /> ··· 208 let spring = useSpring({ opacity: props.active ? 1 : 0 }); 209 return <animated.div style={spring}>{props.children}</animated.div>; 210 };
+71
components/DatePicker.tsx
···
··· 1 + import { ChevronProps, DayPicker as ReactDayPicker } from "react-day-picker"; 2 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 3 + 4 + const CustomChevron = (props: ChevronProps) => { 5 + return ( 6 + <div {...props} className="w-full pointer-events-none"> 7 + <ArrowRightTiny /> 8 + </div> 9 + ); 10 + }; 11 + 12 + interface DayPickerProps { 13 + selected: Date | undefined; 14 + onSelect: (date: Date | undefined) => void; 15 + disabled?: (date: Date) => boolean; 16 + } 17 + 18 + export const DatePicker = ({ 19 + selected, 20 + onSelect, 21 + disabled, 22 + }: DayPickerProps) => { 23 + return ( 24 + <ReactDayPicker 25 + components={{ 26 + Chevron: (props: ChevronProps) => <CustomChevron {...props} />, 27 + }} 28 + classNames={{ 29 + months: "relative", 30 + month_caption: 31 + "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", 32 + button_next: 33 + "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", 34 + button_previous: 35 + "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center", 36 + chevron: "text-inherit", 37 + month_grid: "w-full table-fixed", 38 + weekdays: "text-secondary text-sm", 39 + selected: "bg-accent-1! text-accent-2 rounded-md font-bold", 40 + day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", 41 + outside: "text-tertiary", 42 + today: "font-bold", 43 + disabled: "text-border cursor-not-allowed hover:bg-transparent!", 44 + }} 45 + mode="single" 46 + selected={selected} 47 + defaultMonth={selected} 48 + onSelect={onSelect} 49 + disabled={disabled} 50 + /> 51 + ); 52 + }; 53 + 54 + export const TimePicker = (props: { 55 + value: string; 56 + onChange: (time: string) => void; 57 + className?: string; 58 + }) => { 59 + let handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 60 + props.onChange(e.target.value); 61 + }; 62 + 63 + return ( 64 + <input 65 + type="time" 66 + value={props.value} 67 + onChange={handleTimeChange} 68 + className={`dateBlockTimeInput input-with-border bg-bg-page text-primary w-full ${props.className}`} 69 + /> 70 + ); 71 + };
+84
components/Pages/Backdater.tsx
···
··· 1 + "use client"; 2 + import { DatePicker, TimePicker } from "components/DatePicker"; 3 + import { useMemo, useState } from "react"; 4 + import { timeAgo } from "src/utils/timeAgo"; 5 + import { Popover } from "components/Popover"; 6 + import { Separator } from "react-aria-components"; 7 + import { useReplicache } from "src/replicache"; 8 + import { create } from "zustand"; 9 + 10 + export const useLocalPublishedAt = create<{ [uri: string]: Date }>(() => ({})); 11 + export const Backdater = (props: { publishedAt: string; docURI: string }) => { 12 + let { rep } = useReplicache(); 13 + let localPublishedAtDate = useLocalPublishedAt((s) => 14 + s[props.docURI] ? s[props.docURI] : null, 15 + ); 16 + let localPublishedAt = useMemo( 17 + () => localPublishedAtDate || new Date(props.publishedAt), 18 + [localPublishedAtDate, props.publishedAt], 19 + ); 20 + 21 + let [timeValue, setTimeValue] = useState( 22 + `${localPublishedAt.getHours().toString().padStart(2, "0")}:${localPublishedAt.getMinutes().toString().padStart(2, "0")}`, 23 + ); 24 + 25 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 26 + 27 + const handleTimeChange = async (time: string) => { 28 + setTimeValue(time); 29 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 30 + const newDate = new Date(localPublishedAt); 31 + newDate.setHours(hours); 32 + newDate.setMinutes(minutes); 33 + 34 + let currentDate = new Date(); 35 + if (newDate > currentDate) { 36 + useLocalPublishedAt.setState({ [props.docURI]: currentDate }); 37 + setTimeValue(currentTime); 38 + } else { 39 + useLocalPublishedAt.setState({ [props.docURI]: newDate }); 40 + } 41 + }; 42 + 43 + const handleDateChange = async (date: Date | undefined) => { 44 + if (!date) return; 45 + const [hours, minutes] = timeValue 46 + .split(":") 47 + .map((str) => parseInt(str, 10)); 48 + const newDate = new Date(date); 49 + newDate.setHours(hours); 50 + newDate.setMinutes(minutes); 51 + 52 + let currentDate = new Date(); 53 + if (newDate > currentDate) { 54 + useLocalPublishedAt.setState({ [props.docURI]: currentDate }); 55 + 56 + setTimeValue(currentTime); 57 + } else { 58 + useLocalPublishedAt.setState({ [props.docURI]: newDate }); 59 + } 60 + }; 61 + 62 + return ( 63 + <Popover 64 + className="w-64 z-10 px-2!" 65 + trigger={ 66 + <div className="underline"> 67 + {timeAgo(localPublishedAt.toISOString())} 68 + </div> 69 + } 70 + > 71 + <div className="flex flex-col gap-3"> 72 + <DatePicker 73 + selected={localPublishedAt} 74 + onSelect={handleDateChange} 75 + disabled={(date) => date > new Date()} 76 + /> 77 + <Separator className="border-border" /> 78 + <div className="flex gap-4 pb-1 items-center"> 79 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 80 + </div> 81 + </div> 82 + </Popover> 83 + ); 84 + };
+6 -1
components/Pages/PublicationMetadata.tsx
··· 20 import { TagSelector } from "components/Tags"; 21 import { useIdentityData } from "components/IdentityProvider"; 22 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 23 export const PublicationMetadata = () => { 24 let { rep } = useReplicache(); 25 let { data: pub } = useLeafletPublicationData(); ··· 96 {pub.doc ? ( 97 <div className="flex gap-2 items-center"> 98 <p className="text-sm text-tertiary"> 99 - Published {publishedAt && timeAgo(publishedAt)} 100 </p> 101 102 <Link
··· 20 import { TagSelector } from "components/Tags"; 21 import { useIdentityData } from "components/IdentityProvider"; 22 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 23 + import { Backdater } from "./Backdater"; 24 + 25 export const PublicationMetadata = () => { 26 let { rep } = useReplicache(); 27 let { data: pub } = useLeafletPublicationData(); ··· 98 {pub.doc ? ( 99 <div className="flex gap-2 items-center"> 100 <p className="text-sm text-tertiary"> 101 + Published{" "} 102 + {publishedAt && ( 103 + <Backdater publishedAt={publishedAt} docURI={pub.doc} /> 104 + )} 105 </p> 106 107 <Link
+4 -2
src/replicache/mutations.ts
··· 637 description?: string; 638 tags?: string[]; 639 cover_image?: string | null; 640 }> = async (args, ctx) => { 641 await ctx.runOnServer(async (serverCtx) => { 642 console.log("updating"); ··· 670 } 671 }); 672 await ctx.runOnClient(async ({ tx }) => { 673 - if (args.title !== undefined) 674 - await tx.set("publication_title", args.title); 675 if (args.description !== undefined) 676 await tx.set("publication_description", args.description); 677 if (args.tags !== undefined) await tx.set("publication_tags", args.tags); 678 if (args.cover_image !== undefined) 679 await tx.set("publication_cover_image", args.cover_image); 680 }); 681 }; 682
··· 637 description?: string; 638 tags?: string[]; 639 cover_image?: string | null; 640 + localPublishedAt?: string | null; 641 }> = async (args, ctx) => { 642 await ctx.runOnServer(async (serverCtx) => { 643 console.log("updating"); ··· 671 } 672 }); 673 await ctx.runOnClient(async ({ tx }) => { 674 + if (args.title !== undefined) await tx.set("publication_title", args.title); 675 if (args.description !== undefined) 676 await tx.set("publication_description", args.description); 677 if (args.tags !== undefined) await tx.set("publication_tags", args.tags); 678 if (args.cover_image !== undefined) 679 await tx.set("publication_cover_image", args.cover_image); 680 + if (args.localPublishedAt !== undefined) 681 + await tx.set("publication_local_published_at", args.localPublishedAt); 682 }); 683 }; 684