From 04eb466ef16ad4cbf2147583b2e25c72ec37494e Mon Sep 17 00:00:00 2001 From: celine Date: Tue, 13 Jan 2026 15:35:36 -0500 Subject: [PATCH] added capability to backdate post on published docs --- actions/backdatePost.ts | 86 ++++++++++++++++++++++++ components/Blocks/DateTimeBlock.tsx | 32 +-------- components/DatePicker.tsx | 54 +++++++++++++++ components/Pages/Backdater.tsx | 69 +++++++++++++++++++ components/Pages/PublicationMetadata.tsx | 5 +- 5 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 actions/backdatePost.ts create mode 100644 components/DatePicker.tsx create mode 100644 components/Pages/Backdater.tsx diff --git a/actions/backdatePost.ts b/actions/backdatePost.ts new file mode 100644 index 00000000..f0d688fc --- /dev/null +++ b/actions/backdatePost.ts @@ -0,0 +1,86 @@ +"use server"; + +import { AtpBaseClient, PubLeafletDocument } from "lexicons/api"; +import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; +import { getIdentityData } from "actions/getIdentityData"; +import { supabaseServerClient } from "supabase/serverClient"; +import { Json } from "supabase/database.types"; +import { AtUri } from "@atproto/syntax"; + +type BackdateResult = + | { success: true; publishedAt: string } + | { success: false; error?: OAuthSessionError | string }; + +export async function backdatePost({ + uri, + publishedAt, +}: { + uri: string; + publishedAt: string; +}): Promise { + let identity = await getIdentityData(); + if (!identity || !identity.atp_did) { + return { + success: false, + error: "Not authenticated", + }; + } + + const sessionResult = await restoreOAuthSession(identity.atp_did); + if (!sessionResult.ok) { + return { success: false, error: sessionResult.error }; + } + let credentialSession = sessionResult.value; + let agent = new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession), + ); + + // Get the existing document + let { data: existingDoc } = await supabaseServerClient + .from("documents") + .select("*") + .eq("uri", uri) + .single(); + + if (!existingDoc) { + return { success: false, error: "Document not found" }; + } + + let record = existingDoc.data as PubLeafletDocument.Record; + + // Check if the user is the author + if (record.author !== identity.atp_did) { + return { success: false, error: "Not authorized" }; + } + + let aturi = new AtUri(uri); + + // Update the record with the new publishedAt date + let updatedRecord: PubLeafletDocument.Record = { + ...record, + publishedAt, + }; + + // Update the record on ATP + let result = await agent.com.atproto.repo.putRecord({ + repo: credentialSession.did!, + rkey: aturi.rkey, + record: updatedRecord, + collection: record.$type, + validate: false, + }); + + // Optimistically write to our db + let { error } = await supabaseServerClient + .from("documents") + .update({ + data: updatedRecord as Json, + }) + .eq("uri", uri); + + if (error) { + return { success: false, error: error.message }; + } + + return { success: true, publishedAt }; +} diff --git a/components/Blocks/DateTimeBlock.tsx b/components/Blocks/DateTimeBlock.tsx index 437b0442..ee4f6c7f 100644 --- a/components/Blocks/DateTimeBlock.tsx +++ b/components/Blocks/DateTimeBlock.tsx @@ -1,6 +1,5 @@ import { useEntity, useReplicache } from "src/replicache"; import { BlockProps, BlockLayout } from "./Block"; -import { ChevronProps, DayPicker } from "react-day-picker"; import { Popover } from "components/Popover"; import { useEffect, useMemo, useState } from "react"; import { useEntitySetContext } from "components/EntitySetProvider"; @@ -10,8 +9,8 @@ import { Separator } from "react-aria-components"; import { Checkbox } from "components/Checkbox"; import { useHasPageLoaded } from "components/InitialPageLoadProvider"; import { useSpring, animated } from "@react-spring/web"; -import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; +import { DayPicker } from "components/DatePicker"; export function DateTimeBlock(props: BlockProps) { const [isClient, setIsClient] = useState(false); @@ -168,27 +167,6 @@ export function BaseDateTimeBlock( >
, - }} - classNames={{ - months: "relative", - month_caption: - "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", - button_next: - "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", - button_previous: - "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center ", - chevron: "text-inherit", - month_grid: "w-full table-fixed", - weekdays: "text-secondary text-sm", - selected: "bg-accent-1! text-accent-2 rounded-md font-bold", - - day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", - outside: "text-border", - today: "font-bold", - }} - mode="single" selected={dateFact ? selectedDate : undefined} onSelect={handleDaySelect} /> @@ -230,11 +208,3 @@ let FadeIn = (props: { children: React.ReactNode; active: boolean }) => { let spring = useSpring({ opacity: props.active ? 1 : 0 }); return {props.children}; }; - -const CustomChevron = (props: ChevronProps) => { - return ( -
- -
- ); -}; diff --git a/components/DatePicker.tsx b/components/DatePicker.tsx new file mode 100644 index 00000000..2ec6bfde --- /dev/null +++ b/components/DatePicker.tsx @@ -0,0 +1,54 @@ +import { ChevronProps, DayPicker as ReactDayPicker } from "react-day-picker"; +import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; + +const CustomChevron = (props: ChevronProps) => { + return ( +
+ +
+ ); +}; + +interface DayPickerProps { + selected: Date | undefined; + onSelect: (date: Date | undefined) => void; + disabled?: (date: Date) => boolean; + toDate?: Date; +} + +export const DayPicker = ({ + selected, + onSelect, + disabled, + toDate, +}: DayPickerProps) => { + return ( + , + }} + classNames={{ + months: "relative", + month_caption: + "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", + button_next: + "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", + button_previous: + "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center", + chevron: "text-inherit", + month_grid: "w-full table-fixed", + weekdays: "text-secondary text-sm", + selected: "bg-accent-1! text-accent-2 rounded-md font-bold", + day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", + outside: "text-tertiary", + today: "font-bold", + disabled: "text-border cursor-not-allowed hover:bg-transparent!", + }} + mode="single" + selected={selected} + onSelect={onSelect} + disabled={disabled} + toDate={toDate} + /> + ); +}; diff --git a/components/Pages/Backdater.tsx b/components/Pages/Backdater.tsx new file mode 100644 index 00000000..7c05ab55 --- /dev/null +++ b/components/Pages/Backdater.tsx @@ -0,0 +1,69 @@ +"use client"; +import { DayPicker } from "components/DatePicker"; +import { backdatePost } from "actions/backdatePost"; +import { mutate } from "swr"; +import { DotLoader } from "components/utils/DotLoader"; +import { useToaster } from "components/Toast"; +import { useLeafletPublicationData } from "components/PageSWRDataProvider"; +import { useState } from "react"; +import { timeAgo } from "src/utils/timeAgo"; +import { Popover } from "components/Popover"; + +export const Backdater = (props: { publishedAt: string }) => { + let { data: pub } = useLeafletPublicationData(); + let [isUpdating, setIsUpdating] = useState(false); + let [localPublishedAt, setLocalPublishedAt] = useState(props.publishedAt); + let toaster = useToaster(); + + const handleDaySelect = async (date: Date | undefined) => { + if (!date || !pub?.doc || isUpdating) return; + + // Prevent future dates + if (date > new Date()) return; + + setIsUpdating(true); + try { + const result = await backdatePost({ + uri: pub.doc, + publishedAt: date.toISOString(), + }); + + if (result.success) { + // Update local state immediately + setLocalPublishedAt(date.toISOString()); + // Refresh the publication data + await mutate(`/api/pub/${pub.doc}`); + } + } catch (error) { + console.error("Failed to backdate document:", error); + } finally { + toaster({ + content:
Updated publish date!
, + type: "success", + }); + setIsUpdating(false); + } + }; + + const selectedDate = new Date(localPublishedAt); + + return ( + + ) : ( +
{timeAgo(localPublishedAt)}
+ ) + } + > + date > new Date()} + toDate={new Date()} + /> +
+ ); +}; diff --git a/components/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx index cc111575..3502b371 100644 --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -20,6 +20,8 @@ import { Popover } from "components/Popover"; import { TagSelector } from "components/Tags"; import { useIdentityData } from "components/IdentityProvider"; import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; +import { Backdater } from "./Backdater"; + export const PublicationMetadata = () => { let { rep } = useReplicache(); let { data: pub } = useLeafletPublicationData(); @@ -96,7 +98,8 @@ export const PublicationMetadata = () => { {pub.doc ? (

- Published {publishedAt && timeAgo(publishedAt)} + Published{" "} + {publishedAt && }