a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

celine 3875b4e9 04eb466e

+191 -65
+7 -4
actions/publishToPublication.ts
··· 66 66 tags, 67 67 cover_image, 68 68 entitiesToDelete, 69 + publishedAt, 69 70 }: { 70 71 root_entity: string; 71 72 publication_uri?: string; ··· 75 76 tags?: string[]; 76 77 cover_image?: string | null; 77 78 entitiesToDelete?: string[]; 79 + publishedAt?: string; 78 80 }): Promise<PublishResult> { 79 81 let identity = await getIdentityData(); 80 82 if (!identity || !identity.atp_did) { ··· 147 149 credentialSession.did!, 148 150 ); 149 151 150 - let existingRecord = 151 - (draft?.documents?.data as PubLeafletDocument.Record | undefined) || {}; 152 + let existingRecord = draft?.documents?.data as 153 + | PubLeafletDocument.Record 154 + | undefined; 152 155 153 156 // Extract theme for standalone documents (not for publications) 154 157 let theme: PubLeafletPublication.Theme | undefined; ··· 174 177 } 175 178 176 179 let record: PubLeafletDocument.Record = { 177 - publishedAt: new Date().toISOString(), 178 - ...existingRecord, 179 180 $type: "pub.leaflet.document", 180 181 author: credentialSession.did!, 181 182 ...(publication_uri && { publication: publication_uri }), ··· 199 200 }; 200 201 } 201 202 }), 203 + publishedAt: 204 + existingRecord?.publishedAt || publishedAt || new Date().toISOString(), 202 205 }; 203 206 204 207 // Keep the same rkey if updating an existing document
+112 -8
app/[leaflet_id]/publish/PublishPost.tsx
··· 23 23 import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 24 24 import { PubIcon } from "components/ActionBar/Publications"; 25 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"; 26 31 27 32 type Props = { 28 33 title: string; ··· 78 83 ); 79 84 let [localTags, setLocalTags] = useState<string[]>([]); 80 85 86 + let [localPublishedAt, setLocalPublishedAt] = useState<Date | undefined>( 87 + undefined, 88 + ); 81 89 // Get cover image from Replicache 82 90 let replicacheCoverImage = useSubscribe(rep, (tx) => 83 91 tx.get<string | null>("publication_cover_image"), ··· 116 124 tags: currentTags, 117 125 cover_image: replicacheCoverImage, 118 126 entitiesToDelete: props.entitiesToDelete, 127 + publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 119 128 }); 120 129 121 130 if (!result.success) { ··· 168 177 record={props.record} 169 178 /> 170 179 <hr className="border-border" /> 171 - <ShareOptions 172 - setShareOption={setShareOption} 173 - shareOption={shareOption} 174 - charCount={charCount} 175 - setCharCount={setCharCount} 176 - editorStateRef={editorStateRef} 177 - {...props} 180 + 181 + <BackdateOptions 182 + publishedAt={localPublishedAt} 183 + setPublishedAt={setLocalPublishedAt} 178 184 /> 179 185 <hr className="border-border " /> 186 + 180 187 <div className="flex flex-col gap-2"> 181 188 <h4>Tags</h4> 182 189 <TagSelector ··· 184 191 setSelectedTags={handleTagsChange} 185 192 /> 186 193 </div> 194 + <hr className="border-border" /> 195 + <ShareOptions 196 + setShareOption={setShareOption} 197 + shareOption={shareOption} 198 + charCount={charCount} 199 + setCharCount={setCharCount} 200 + editorStateRef={editorStateRef} 201 + {...props} 202 + /> 187 203 <hr className="border-border mb-2" /> 188 204 189 205 <div className="flex flex-col gap-2"> ··· 219 235 ); 220 236 }; 221 237 238 + const BackdateOptions = (props: { 239 + publishedAt: Date | undefined; 240 + setPublishedAt: (date: Date | undefined) => void; 241 + }) => { 242 + const formattedDate = useLocalizedDate( 243 + props.publishedAt?.toISOString() || "", 244 + { 245 + month: "short", 246 + day: "numeric", 247 + year: "numeric", 248 + hour: "numeric", 249 + minute: "numeric", 250 + hour12: true, 251 + }, 252 + ); 253 + 254 + const [timeValue, setTimeValue] = useState<string>(() => { 255 + if (!props.publishedAt) return "12:00"; 256 + return `${props.publishedAt.getHours().toString().padStart(2, "0")}:${props.publishedAt.getMinutes().toString().padStart(2, "0")}`; 257 + }); 258 + 259 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 260 + 261 + const handleTimeChange = (time: string) => { 262 + setTimeValue(time); 263 + if (!props.publishedAt) return; 264 + 265 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 266 + const newDate = setHours(setMinutes(props.publishedAt, minutes), hours); 267 + const currentDate = new Date(); 268 + 269 + if (newDate > currentDate) { 270 + props.setPublishedAt(currentDate); 271 + setTimeValue(currentTime); 272 + } else props.setPublishedAt(newDate); 273 + }; 274 + 275 + const handleDateChange = (date: Date | undefined) => { 276 + if (!date) { 277 + props.setPublishedAt(undefined); 278 + return; 279 + } 280 + const [hours, minutes] = timeValue 281 + .split(":") 282 + .map((str) => parseInt(str, 10)); 283 + const newDate = new Date( 284 + date.getFullYear(), 285 + date.getMonth(), 286 + date.getDate(), 287 + hours, 288 + minutes, 289 + ); 290 + const currentDate = new Date(); 291 + if (newDate > currentDate) { 292 + props.setPublishedAt(currentDate); 293 + setTimeValue(currentTime); 294 + } else props.setPublishedAt(newDate); 295 + }; 296 + 297 + return ( 298 + <div className="flex justify-between gap-2"> 299 + <h4>Publish Date</h4> 300 + <Popover 301 + className="w-64 px-2!" 302 + trigger={ 303 + props.publishedAt ? ( 304 + <div className="text-secondary">{formattedDate}</div> 305 + ) : ( 306 + <div className="text-tertiary italic">now</div> 307 + ) 308 + } 309 + > 310 + <div className="flex flex-col gap-3"> 311 + <DatePicker 312 + selected={props.publishedAt} 313 + onSelect={handleDateChange} 314 + disabled={(date) => date > new Date()} 315 + /> 316 + <Separator className="border-border" /> 317 + <div className="flex gap-4 pb-1 items-center"> 318 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 319 + </div> 320 + </div> 321 + </Popover> 322 + </div> 323 + ); 324 + }; 325 + 222 326 const ShareOptions = (props: { 223 327 shareOption: "quiet" | "bluesky"; 224 328 setShareOption: (option: typeof props.shareOption) => void; ··· 232 336 }) => { 233 337 return ( 234 338 <div className="flex flex-col gap-2"> 235 - <h4>Notifications</h4> 339 + <h4>Share and Notify</h4> 236 340 <Radio 237 341 checked={props.shareOption === "quiet"} 238 342 onChange={(e) => {
+2 -2
components/Blocks/DateTimeBlock.tsx
··· 10 10 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 11 11 import { useSpring, animated } from "@react-spring/web"; 12 12 import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; 13 - import { DayPicker } from "components/DatePicker"; 13 + import { DatePicker } from "components/DatePicker"; 14 14 15 15 export function DateTimeBlock(props: BlockProps) { 16 16 const [isClient, setIsClient] = useState(false); ··· 166 166 } 167 167 > 168 168 <div className="flex flex-col gap-3 "> 169 - <DayPicker 169 + <DatePicker 170 170 selected={dateFact ? selectedDate : undefined} 171 171 onSelect={handleDaySelect} 172 172 />
+20 -4
components/DatePicker.tsx
··· 13 13 selected: Date | undefined; 14 14 onSelect: (date: Date | undefined) => void; 15 15 disabled?: (date: Date) => boolean; 16 - toDate?: Date; 17 16 } 18 17 19 - export const DayPicker = ({ 18 + export const DatePicker = ({ 20 19 selected, 21 20 onSelect, 22 21 disabled, 23 - toDate, 24 22 }: DayPickerProps) => { 25 23 return ( 26 24 <ReactDayPicker ··· 48 46 selected={selected} 49 47 onSelect={onSelect} 50 48 disabled={disabled} 51 - toDate={toDate} 49 + /> 50 + ); 51 + }; 52 + 53 + export const TimePicker = (props: { 54 + value: string; 55 + onChange: (time: string) => void; 56 + className?: string; 57 + }) => { 58 + let handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 59 + props.onChange(e.target.value); 60 + }; 61 + 62 + return ( 63 + <input 64 + type="time" 65 + value={props.value} 66 + onChange={handleTimeChange} 67 + className={`dateBlockTimeInput input-with-border bg-bg-page text-primary w-full ${props.className}`} 52 68 /> 53 69 ); 54 70 };
+50 -47
components/Pages/Backdater.tsx
··· 1 1 "use client"; 2 - import { DayPicker } from "components/DatePicker"; 3 - import { backdatePost } from "actions/backdatePost"; 4 - import { mutate } from "swr"; 5 - import { DotLoader } from "components/utils/DotLoader"; 6 - import { useToaster } from "components/Toast"; 7 - import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 2 + import { DatePicker, TimePicker } from "components/DatePicker"; 8 3 import { useState } from "react"; 9 4 import { timeAgo } from "src/utils/timeAgo"; 10 5 import { Popover } from "components/Popover"; 6 + import { Separator } from "react-aria-components"; 11 7 12 8 export const Backdater = (props: { publishedAt: string }) => { 13 - let { data: pub } = useLeafletPublicationData(); 14 - let [isUpdating, setIsUpdating] = useState(false); 15 - let [localPublishedAt, setLocalPublishedAt] = useState(props.publishedAt); 16 - let toaster = useToaster(); 9 + let [localPublishedAt, setLocalPublishedAt] = useState( 10 + new Date(props.publishedAt), 11 + ); 17 12 18 - const handleDaySelect = async (date: Date | undefined) => { 19 - if (!date || !pub?.doc || isUpdating) return; 13 + let [timeValue, setTimeValue] = useState( 14 + `${localPublishedAt.getHours().toString().padStart(2, "0")}:${localPublishedAt.getMinutes().toString().padStart(2, "0")}`, 15 + ); 20 16 21 - // Prevent future dates 22 - if (date > new Date()) return; 17 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 23 18 24 - setIsUpdating(true); 25 - try { 26 - const result = await backdatePost({ 27 - uri: pub.doc, 28 - publishedAt: date.toISOString(), 29 - }); 19 + const handleTimeChange = (time: string) => { 20 + setTimeValue(time); 21 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 22 + const newDate = new Date(localPublishedAt); 23 + newDate.setHours(hours); 24 + newDate.setMinutes(minutes); 30 25 31 - if (result.success) { 32 - // Update local state immediately 33 - setLocalPublishedAt(date.toISOString()); 34 - // Refresh the publication data 35 - await mutate(`/api/pub/${pub.doc}`); 36 - } 37 - } catch (error) { 38 - console.error("Failed to backdate document:", error); 39 - } finally { 40 - toaster({ 41 - content: <div className="font-bold">Updated publish date!</div>, 42 - type: "success", 43 - }); 44 - setIsUpdating(false); 45 - } 26 + let currentDate = new Date(); 27 + if (newDate > currentDate) { 28 + setLocalPublishedAt(currentDate); 29 + setTimeValue(currentTime); 30 + } else setLocalPublishedAt(newDate); 46 31 }; 47 32 48 - const selectedDate = new Date(localPublishedAt); 33 + const handleDateChange = (date: Date | undefined) => { 34 + if (!date) return; 35 + const [hours, minutes] = timeValue 36 + .split(":") 37 + .map((str) => parseInt(str, 10)); 38 + const newDate = new Date(date); 39 + newDate.setHours(hours); 40 + newDate.setMinutes(minutes); 41 + 42 + let currentDate = new Date(); 43 + if (newDate > currentDate) { 44 + setLocalPublishedAt(currentDate); 45 + setTimeValue(currentTime); 46 + } else setLocalPublishedAt(newDate); 47 + }; 48 + console.log(localPublishedAt); 49 49 50 50 return ( 51 51 <Popover 52 52 className="w-64 z-10 px-2!" 53 53 trigger={ 54 - isUpdating ? ( 55 - <DotLoader className="h-[21px]!" /> 56 - ) : ( 57 - <div className="underline">{timeAgo(localPublishedAt)}</div> 58 - ) 54 + <div className="underline"> 55 + {timeAgo(localPublishedAt.toISOString())} 56 + </div> 59 57 } 60 58 > 61 - <DayPicker 62 - selected={selectedDate} 63 - onSelect={handleDaySelect} 64 - disabled={(date) => date > new Date()} 65 - toDate={new Date()} 66 - /> 59 + <div className="flex flex-col gap-3"> 60 + <DatePicker 61 + selected={localPublishedAt} 62 + onSelect={handleDateChange} 63 + disabled={(date) => date > new Date()} 64 + /> 65 + <Separator className="border-border" /> 66 + <div className="flex gap-4 pb-1 items-center"> 67 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 68 + </div> 69 + </div> 67 70 </Popover> 68 71 ); 69 72 };