From 6ec361b8f73f7fa9764e6a37d2a244ee43902fed Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 7 Aug 2025 22:47:35 -0700 Subject: [PATCH 1/7] Add timestamp tooltip --- packages/ui/src/index.tsx | 1 + packages/ui/src/timestamp-tooltip.tsx | 115 ++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 packages/ui/src/timestamp-tooltip.tsx diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index a142ca1feca..24b905b3f6c 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -35,6 +35,7 @@ export * from "./smart-datetime-picker"; export * from "./status-badge"; export * from "./switch"; export * from "./table"; +export * from "./timestamp-tooltip"; export * from "./toggle-group"; export * from "./tooltip"; export * from "./utm-builder"; diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx new file mode 100644 index 00000000000..77a6b52cd98 --- /dev/null +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { cn, timeAgo } from "@dub/utils"; +import { Tooltip } from "./tooltip"; + +type TimestampInput = Date | string | number; + +export interface TimestampTooltipProps { + timestamp: TimestampInput; + /** + * If true, the inline display will only render the time (HH:mm:ss). + * Useful for compact layouts on mobile. + */ + timeOnly?: boolean; + side?: "top" | "bottom" | "left" | "right"; + className?: string; +} + +function getLocalTimeZone(): string { + if (typeof Intl !== "undefined") { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "Local"; + } catch (e) { + return "Local"; + } + } + return "Local"; +} + +function formatDisplay(date: Date, timeOnly?: boolean) { + const time = date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + if (timeOnly) return time; + + // Month (short, uppercase) + 2-digit day + const datePart = date + .toLocaleDateString("en-US", { month: "short", day: "2-digit" }) + .toUpperCase(); + return `${datePart} ${time}`; +} + +export function TimestampTooltip({ + timestamp, + timeOnly, + className, + side = "top", + ...tooltipProps +}: TimestampTooltipProps) { + const date = new Date(timestamp); + + const tz = getLocalTimeZone(); + + const inlineText = formatDisplay(date, timeOnly); + + const commonFormat: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: true, + }; + + const localText = date.toLocaleString("en-US", commonFormat); + + const utcText = new Date(date.getTime()).toLocaleString("en-US", { + ...commonFormat, + timeZone: "UTC", + }); + + const unixMs = date.getTime().toString(); + const relative = timeAgo(date, { withAgo: true }); + + const rows: { label: string; value: string }[] = [ + { label: tz, value: localText }, + { label: "UTC", value: utcText }, + { label: "UNIX Timestamp", value: unixMs }, + { label: "Relative", value: relative }, + ]; + + return ( + + {rows.map((row, idx) => ( +
+ + {row.label} + + + {row.value} + +
+ ))} + + } + {...tooltipProps} + > + + {inlineText} + +
+ ); +} + +export default TimestampTooltip; From 156841656cf2661eb20e1489ebba56bb29c2ef1c Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 17 Oct 2025 15:58:18 -0400 Subject: [PATCH 2/7] Add timestamp tooltips throughout apps --- .../(enrolled)/earnings/earnings-table.tsx | 13 +- .../[bountyId]/bounty-submissions-table.tsx | 18 +- .../[campaignId]/campaign-events-columns.tsx | 12 +- .../program/campaigns/campaigns-table.tsx | 20 +- .../program/commissions/commission-table.tsx | 14 +- .../(ee)/program/partners/partners-table.tsx | 14 +- .../[slug]/links/utm/template-card.tsx | 10 +- apps/web/ui/analytics/events/events-table.tsx | 20 +- .../ui/customers/customer-activity-list.tsx | 15 +- .../ui/customers/customer-details-column.tsx | 22 +- .../web/ui/customers/customer-sales-table.tsx | 13 +- .../links/link-builder/link-creator-info.tsx | 15 +- apps/web/ui/links/link-title-column.tsx | 6 +- packages/ui/src/timestamp-tooltip.tsx | 199 ++++++++++++------ 14 files changed, 258 insertions(+), 133 deletions(-) diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx index 9f385a9ef0a..f9281a7525d 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx @@ -14,6 +14,7 @@ import { LinkLogo, StatusBadge, Table, + TimestampTooltip, Tooltip, usePagination, useRouterStuff, @@ -24,7 +25,6 @@ import { cn, currencyFormatter, fetcher, - formatDateTime, formatDateTimeSmart, getApexDomain, getPrettyUrl, @@ -79,9 +79,14 @@ export function EarningsTablePartner({ limit }: { limit?: number }) { accessorKey: "timestamp", minSize: 140, cell: ({ row }) => ( -

- {formatDateTimeSmart(row.original.createdAt)} -

+ + {formatDateTimeSmart(row.original.createdAt)} + ), }, { diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx index ba1904fd985..8dc54827db8 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/[bountyId]/bounty-submissions-table.tsx @@ -16,6 +16,7 @@ import { ProgressCircle, StatusBadge, Table, + TimestampTooltip, usePagination, useRouterStuff, useTable, @@ -185,12 +186,21 @@ export function BountySubmissionsTable() { { id: "createdAt", header: "Submitted", - accessorFn: (d: BountySubmissionProps) => { - if (!d.createdAt || d.status === "draft") { + cell: ({ row }) => { + if (!row.original.createdAt || row.original.status === "draft") return "-"; - } - return formatDate(d.createdAt, { month: "short" }); + return ( + + + {formatDate(row.original.createdAt, { month: "short" })} + + + ); }, }, ] diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx index 96d4a0a8ab7..80f6aea2eb8 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx @@ -1,6 +1,6 @@ import { GroupColorCircle } from "@/ui/partners/groups/group-color-circle"; -import { Tooltip } from "@dub/ui"; -import { formatDateTime, OG_AVATAR_URL, timeAgo } from "@dub/utils"; +import { TimestampTooltip } from "@dub/ui"; +import { OG_AVATAR_URL, timeAgo } from "@dub/utils"; import { CampaignEvent } from "./campaign-events"; export const campaignEventsColumns = [ @@ -45,9 +45,11 @@ export const campaignEventsColumns = [ const timestamp = getTimestamp(row.original); return ( -
{timeAgo(timestamp)}
-
+ ); }, }, diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx index 59b6846fc79..1faccec1bed 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx @@ -17,19 +17,14 @@ import { Popover, StatusBadge, Table, + TimestampTooltip, Tooltip, usePagination, useRouterStuff, useTable, } from "@dub/ui"; import { Dots, Duplicate, LoadingCircle, Trash } from "@dub/ui/icons"; -import { - cn, - fetcher, - formatDateTime, - formatDateTimeSmart, - nFormatter, -} from "@dub/utils"; +import { cn, fetcher, formatDateTimeSmart, nFormatter } from "@dub/utils"; import { Row } from "@tanstack/react-table"; import { Command } from "cmdk"; import { Mail, Pause, Play } from "lucide-react"; @@ -121,9 +116,14 @@ export function CampaignsTable() { header: "Created", accessorFn: (d) => d.createdAt, cell: ({ row }) => ( -

- {formatDateTimeSmart(row.original.createdAt)} -

+ + {formatDateTimeSmart(row.original.createdAt)} + ), }, { diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx index bb484c58d4d..0822b98d12e 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/commissions/commission-table.tsx @@ -18,6 +18,7 @@ import { Filter, StatusBadge, Table, + TimestampTooltip, Tooltip, usePagination, useRouterStuff, @@ -28,7 +29,6 @@ import { cn, currencyFormatter, fetcher, - formatDateTime, formatDateTimeSmart, nFormatter, } from "@dub/utils"; @@ -90,9 +90,15 @@ const CommissionTableInner = memo( id: "createdAt", header: "Date", cell: ({ row }) => ( -

- {formatDateTimeSmart(row.original.createdAt)} -

+ +

{formatDateTimeSmart(row.original.createdAt)}

+
), }, { diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx index 66bcbc12125..89b7a780db3 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx @@ -28,6 +28,7 @@ import { Popover, StatusBadge, Table, + TimestampTooltip, useColumnVisibility, usePagination, useRouterStuff, @@ -191,7 +192,18 @@ export function PartnersTable() { { id: "createdAt", header: "Enrolled", - accessorFn: (d) => formatDate(d.createdAt, { month: "short" }), + cell: ({ row }) => ( + + + {formatDate(row.original.createdAt, { month: "short" })} + + + ), }, { id: "status", diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/utm/template-card.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/utm/template-card.tsx index 414899fc853..f23b90c447a 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/links/utm/template-card.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/links/utm/template-card.tsx @@ -9,6 +9,7 @@ import { Button, CardList, Popover, + TimestampTooltip, Tooltip, useKeyboardShortcut, UTM_PARAMETERS, @@ -108,7 +109,14 @@ export function TemplateCard({
- {formatDate(template.updatedAt, { month: "short" })} + + {formatDate(template.updatedAt, { month: "short" })} +
diff --git a/apps/web/ui/analytics/events/events-table.tsx b/apps/web/ui/analytics/events/events-table.tsx index 291e58c3f7a..86ec3091ce6 100644 --- a/apps/web/ui/analytics/events/events-table.tsx +++ b/apps/web/ui/analytics/events/events-table.tsx @@ -12,6 +12,7 @@ import { EditColumnsButton, LinkLogo, Table, + TimestampTooltip, Tooltip, useColumnVisibility, usePagination, @@ -184,21 +185,16 @@ export default function EventsTable({ enableHiding: false, size: 160, cell: ({ getValue }) => ( - -
+
{formatDateTimeSmart(getValue())}
- + ), }, // Sale amount diff --git a/apps/web/ui/customers/customer-activity-list.tsx b/apps/web/ui/customers/customer-activity-list.tsx index de7d52aecc8..16d393ef6c7 100644 --- a/apps/web/ui/customers/customer-activity-list.tsx +++ b/apps/web/ui/customers/customer-activity-list.tsx @@ -1,5 +1,5 @@ import { CustomerActivityResponse } from "@/lib/types"; -import { DynamicTooltipWrapper, LinkLogo } from "@dub/ui"; +import { DynamicTooltipWrapper, LinkLogo, TimestampTooltip } from "@dub/ui"; import { CursorRays, MoneyBill2, UserCheck } from "@dub/ui/icons"; import { formatDateTimeSmart, getApexDomain, getPrettyUrl } from "@dub/utils"; import Link from "next/link"; @@ -136,9 +136,16 @@ export function CustomerActivityList({
{content(event)}
- - {formatDateTimeSmart(event.timestamp)} - + + + {formatDateTimeSmart(event.timestamp)} + +
); diff --git a/apps/web/ui/customers/customer-details-column.tsx b/apps/web/ui/customers/customer-details-column.tsx index 2ab9a2fb3ba..a5027cee12a 100644 --- a/apps/web/ui/customers/customer-details-column.tsx +++ b/apps/web/ui/customers/customer-details-column.tsx @@ -1,5 +1,5 @@ import { CustomerActivityResponse, CustomerProps } from "@/lib/types"; -import { CopyButton, UTM_PARAMETERS } from "@dub/ui"; +import { CopyButton, TimestampTooltip, UTM_PARAMETERS } from "@dub/ui"; import { capitalize, cn, @@ -123,13 +123,19 @@ export function CustomerDetailsColumn({
Customer since {customer ? ( - - {new Date(customer.createdAt).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} - + + + {new Date(customer.createdAt).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + ) : (
)} diff --git a/apps/web/ui/customers/customer-sales-table.tsx b/apps/web/ui/customers/customer-sales-table.tsx index 9d5ee0f4f23..15ccb314e96 100644 --- a/apps/web/ui/customers/customer-sales-table.tsx +++ b/apps/web/ui/customers/customer-sales-table.tsx @@ -1,5 +1,5 @@ import { CommissionResponse, SaleEvent } from "@/lib/types"; -import { StatusBadge } from "@dub/ui"; +import { StatusBadge, TimestampTooltip } from "@dub/ui"; import { currencyFormatter, formatDateTimeSmart, nFormatter } from "@dub/utils"; import { flexRender, @@ -37,7 +37,16 @@ export function CustomerSalesTable({ new Date("timestamp" in d ? d.timestamp : d.createdAt), enableHiding: false, minSize: 100, - cell: ({ getValue }) => {formatDateTimeSmart(getValue())}, + cell: ({ getValue }) => ( + + {formatDateTimeSmart(getValue())} + + ), }, ...(sales?.length && "eventName" in sales?.[0] ? [ diff --git a/apps/web/ui/links/link-builder/link-creator-info.tsx b/apps/web/ui/links/link-builder/link-creator-info.tsx index 6c6012bb4a8..6f833760f0a 100644 --- a/apps/web/ui/links/link-builder/link-creator-info.tsx +++ b/apps/web/ui/links/link-builder/link-creator-info.tsx @@ -1,7 +1,7 @@ import { ExpandedLinkProps } from "@/lib/types"; import { UserAvatar } from "@/ui/links/link-title-column"; -import { Tooltip } from "@dub/ui"; -import { formatDateTime, timeAgo } from "@dub/utils"; +import { TimestampTooltip } from "@dub/ui"; +import { timeAgo } from "@dub/utils"; import Link from "next/link"; import { useParams } from "next/navigation"; @@ -28,9 +28,14 @@ export function LinkCreatorInfo({ link }: { link: ExpandedLinkProps }) { ยท{" "} - - {timeAgo(link.createdAt)} - + + {timeAgo(link.createdAt)} +
); diff --git a/apps/web/ui/links/link-title-column.tsx b/apps/web/ui/links/link-title-column.tsx index f6d52612d5d..7d485739d41 100644 --- a/apps/web/ui/links/link-title-column.tsx +++ b/apps/web/ui/links/link-title-column.tsx @@ -12,6 +12,7 @@ import { CopyButton, LinkLogo, Switch, + TimestampTooltip, Tooltip, TooltipContent, useInViewport, @@ -32,7 +33,6 @@ import { } from "@dub/ui/icons"; import { cn, - formatDateTime, getApexDomain, getPrettyUrl, isDubDomain, @@ -381,9 +381,9 @@ const Details = memo( displayProperties.includes("createdAt") && "sm:block", )} > - + {timeAgo(createdAt)} - +
); diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx index 77a6b52cd98..c2b6e4fca47 100644 --- a/packages/ui/src/timestamp-tooltip.tsx +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -1,60 +1,43 @@ "use client"; -import { cn, timeAgo } from "@dub/utils"; -import { Tooltip } from "./tooltip"; - -type TimestampInput = Date | string | number; - -export interface TimestampTooltipProps { - timestamp: TimestampInput; - /** - * If true, the inline display will only render the time (HH:mm:ss). - * Useful for compact layouts on mobile. - */ - timeOnly?: boolean; - side?: "top" | "bottom" | "left" | "right"; +import { cn } from "@dub/utils"; +import { formatDuration, intervalToDuration } from "date-fns"; +import { useMemo } from "react"; +import { toast } from "sonner"; +import { Tooltip, TooltipProps } from "./tooltip"; + +const DAY_MS = 24 * 60 * 60 * 1000; +const MONTH_MS = 30 * DAY_MS; + +export type TimestampTooltipProps = { + timestamp: Date | string | number | null | undefined; + rows?: ("local" | "utc" | "unix")[]; + interactive?: boolean; className?: string; -} +} & Omit; function getLocalTimeZone(): string { if (typeof Intl !== "undefined") { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "Local"; - } catch (e) { - return "Local"; - } + } catch (e) {} } return "Local"; } -function formatDisplay(date: Date, timeOnly?: boolean) { - const time = date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - if (timeOnly) return time; - - // Month (short, uppercase) + 2-digit day - const datePart = date - .toLocaleDateString("en-US", { month: "short", day: "2-digit" }) - .toUpperCase(); - return `${datePart} ${time}`; -} - export function TimestampTooltip({ timestamp, - timeOnly, + children, + rows = ["local", "utc"], + interactive = false, className, - side = "top", ...tooltipProps }: TimestampTooltipProps) { - const date = new Date(timestamp); + if (!timestamp) return children; - const tz = getLocalTimeZone(); + const date = new Date(timestamp); - const inlineText = formatDisplay(date, timeOnly); + if (date.toString() === "Invalid Date") return children; const commonFormat: Intl.DateTimeFormatOptions = { year: "numeric", @@ -66,48 +49,124 @@ export function TimestampTooltip({ hour12: true, }; - const localText = date.toLocaleString("en-US", commonFormat); - - const utcText = new Date(date.getTime()).toLocaleString("en-US", { - ...commonFormat, - timeZone: "UTC", + const diff = new Date().getTime() - date.getTime(); + const relativeDuration = intervalToDuration({ + start: date, + end: new Date(), }); + const relative = + formatDuration(relativeDuration, { + delimiter: ", ", + format: [ + "years", + "months", + "days", + ...(diff < MONTH_MS + ? [ + "hours" as const, + ...(diff < DAY_MS + ? ["minutes" as const, "seconds" as const] + : []), + ] + : []), + ], + }) + " ago"; + + const items: { + label: string; + shortLabel?: string; + value: string; + valueMono?: boolean; + }[] = useMemo( + () => + rows.map( + (key) => + ({ + local: { + label: getLocalTimeZone(), + shortLabel: new Date() + .toLocaleTimeString("en-US", { timeZoneName: "short" }) + .split(" ")[2], + value: date.toLocaleString("en-US", commonFormat), + }, + + utc: { + label: "UTC", + shortLabel: "UTC", + value: new Date(date.getTime()).toLocaleString("en-US", { + ...commonFormat, + timeZone: "UTC", + }), + }, + + unix: { + label: "UNIX Timestamp", + value: date.getTime().toString(), + valueMono: true, + }, + })[key]!, + ), + [rows, date], + ); - const unixMs = date.getTime().toString(); - const relative = timeAgo(date, { withAgo: true }); - - const rows: { label: string; value: string }[] = [ - { label: tz, value: localText }, - { label: "UTC", value: utcText }, - { label: "UNIX Timestamp", value: unixMs }, - { label: "Relative", value: relative }, - ]; + const shortLabels = items.every(({ shortLabel }) => shortLabel); return ( - {rows.map((row, idx) => ( -
- - {row.label} - - - {row.value} - -
- ))} +
+ {relative} + + {items.map((row, idx) => ( + { + try { + navigator.clipboard.writeText(row.value); + toast.success("Copied to clipboard"); + } catch (e) { + toast.error("Failed to copy to clipboard"); + console.error("Failed to copy to clipboard", e); + } + } + : undefined + } + > + + + + ))} +
+ + {shortLabels ? row.shortLabel : row.label} + + + {row.value} +
} + disableHoverableContent={!interactive} {...tooltipProps} > - - {inlineText} - + {children}
); } From a5bcdd437647bb4b6b090fa216077b04e21a1df0 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 17 Oct 2025 16:16:18 -0400 Subject: [PATCH 3/7] Misc. fixes --- apps/web/ui/links/link-title-column.tsx | 6 +++++- packages/ui/src/timestamp-tooltip.tsx | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/web/ui/links/link-title-column.tsx b/apps/web/ui/links/link-title-column.tsx index 7d485739d41..21c672b64f7 100644 --- a/apps/web/ui/links/link-title-column.tsx +++ b/apps/web/ui/links/link-title-column.tsx @@ -381,7 +381,11 @@ const Details = memo( displayProperties.includes("createdAt") && "sm:block", )} > - + {timeAgo(createdAt)} diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx index c2b6e4fca47..ba3f67ecb45 100644 --- a/packages/ui/src/timestamp-tooltip.tsx +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -25,7 +25,17 @@ function getLocalTimeZone(): string { return "Local"; } -export function TimestampTooltip({ +export function TimestampTooltip(props: TimestampTooltipProps) { + if ( + !props.timestamp || + new Date(props.timestamp).toString() === "Invalid Date" + ) + return props.children; + + return ; +} + +function TimestampTooltipContent({ timestamp, children, rows = ["local", "utc"], @@ -33,12 +43,11 @@ export function TimestampTooltip({ className, ...tooltipProps }: TimestampTooltipProps) { - if (!timestamp) return children; + if (!timestamp) + throw new Error("Falsy timestamp not permitted in TimestampTooltipContent"); const date = new Date(timestamp); - if (date.toString() === "Invalid Date") return children; - const commonFormat: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", @@ -126,9 +135,9 @@ export function TimestampTooltip({ )} onClick={ interactive - ? () => { + ? async () => { try { - navigator.clipboard.writeText(row.value); + await navigator.clipboard.writeText(row.value); toast.success("Copied to clipboard"); } catch (e) { toast.error("Failed to copy to clipboard"); From bb5794856c423dd435b7655e45cb0e824bd80a32 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 17 Oct 2025 16:43:38 -0400 Subject: [PATCH 4/7] More updates --- apps/web/ui/analytics/events/events-table.tsx | 4 +- packages/ui/src/timestamp-tooltip.tsx | 142 ++++++++++-------- 2 files changed, 79 insertions(+), 67 deletions(-) diff --git a/apps/web/ui/analytics/events/events-table.tsx b/apps/web/ui/analytics/events/events-table.tsx index 86ec3091ce6..9d9b36896d0 100644 --- a/apps/web/ui/analytics/events/events-table.tsx +++ b/apps/web/ui/analytics/events/events-table.tsx @@ -191,9 +191,9 @@ export default function EventsTable({ rows={["local", "utc", "unix"]} interactive > -
+ {formatDateTimeSmart(getValue())} -
+
), }, diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx index ba3f67ecb45..9d865483a82 100644 --- a/packages/ui/src/timestamp-tooltip.tsx +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -2,7 +2,7 @@ import { cn } from "@dub/utils"; import { formatDuration, intervalToDuration } from "date-fns"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Tooltip, TooltipProps } from "./tooltip"; @@ -25,24 +25,35 @@ function getLocalTimeZone(): string { return "Local"; } -export function TimestampTooltip(props: TimestampTooltipProps) { - if ( - !props.timestamp || - new Date(props.timestamp).toString() === "Invalid Date" - ) - return props.children; +export function TimestampTooltip({ + timestamp, + rows, + interactive, + ...tooltipProps +}: TimestampTooltipProps) { + if (!timestamp || new Date(timestamp).toString() === "Invalid Date") + return tooltipProps.children; - return ; + return ( + + } + disableHoverableContent={!interactive} + {...tooltipProps} + /> + ); } function TimestampTooltipContent({ timestamp, - children, rows = ["local", "utc"], interactive = false, - className, - ...tooltipProps -}: TimestampTooltipProps) { +}: Pick) { if (!timestamp) throw new Error("Falsy timestamp not permitted in TimestampTooltipContent"); @@ -120,63 +131,64 @@ function TimestampTooltipContent({ const shortLabels = items.every(({ shortLabel }) => shortLabel); + // Re-render every second to update the relative time + const [_, setRenderCount] = useState(0); + useEffect(() => { + const interval = setInterval(() => setRenderCount((c) => c + 1), 1000); + return () => clearInterval(interval); + }, []); + return ( - - {relative} - - {items.map((row, idx) => ( - + {diff > 0 && ( + {relative} + )} +
+ {items.map((row, idx) => ( + { + try { + await navigator.clipboard.writeText(row.value); + toast.success("Copied to clipboard"); + } catch (e) { + toast.error("Failed to copy to clipboard"); + console.error("Failed to copy to clipboard", e); + } + } + : undefined + } + > + - - - ))} -
+ { - try { - await navigator.clipboard.writeText(row.value); - toast.success("Copied to clipboard"); - } catch (e) { - toast.error("Failed to copy to clipboard"); - console.error("Failed to copy to clipboard", e); - } - } - : undefined - } + title={shortLabels ? row.label : undefined} > - - - {shortLabels ? row.shortLabel : row.label} - - - {row.value} -
- - } - disableHoverableContent={!interactive} - {...tooltipProps} - > - {children} -
+ {shortLabels ? row.shortLabel : row.label} + + + + {row.value} + + + ))} + + ); } From f45aedbcb742cb33079cbdc58aabf79892a3e024 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 17 Oct 2025 16:53:25 -0400 Subject: [PATCH 5/7] Update customer-table.tsx --- .../ui/customers/customer-table/customer-table.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/ui/customers/customer-table/customer-table.tsx b/apps/web/ui/customers/customer-table/customer-table.tsx index bfb517445c9..767ffad2c8b 100644 --- a/apps/web/ui/customers/customer-table/customer-table.tsx +++ b/apps/web/ui/customers/customer-table/customer-table.tsx @@ -19,6 +19,7 @@ import { MenuItem, Popover, Table, + TimestampTooltip, useColumnVisibility, useCopyToClipboard, usePagination, @@ -171,7 +172,18 @@ export function CustomerTable() { { id: "createdAt", header: "Created", - accessorFn: (d) => formatDate(d.createdAt, { month: "short" }), + cell: ({ row }) => ( + + + {formatDate(row.original.createdAt, { month: "short" })} + + + ), }, { id: "link", From 008df8bd787306240409013fe8a6449d240b4e80 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 17 Oct 2025 16:54:26 -0400 Subject: [PATCH 6/7] Update timestamp-tooltip.tsx --- packages/ui/src/timestamp-tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx index 9d865483a82..d64cd3c53c0 100644 --- a/packages/ui/src/timestamp-tooltip.tsx +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -121,7 +121,7 @@ function TimestampTooltipContent({ unix: { label: "UNIX Timestamp", - value: date.getTime().toString(), + value: (date.getTime() / 1000).toString(), valueMono: true, }, })[key]!, From c65cbbfaed0bb22f97f5791b773f1bcc1cb1ce20 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 17 Oct 2025 14:54:06 -0700 Subject: [PATCH 7/7] make interactive true by default, improve timestamp copy UX --- .../(enrolled)/earnings/earnings-table.tsx | 1 - .../[campaignId]/campaign-events-columns.tsx | 1 - .../program/commissions/commission-table.tsx | 1 - apps/web/ui/analytics/events/events-table.tsx | 1 - .../ui/customers/customer-activity-list.tsx | 1 - .../web/ui/customers/customer-sales-table.tsx | 1 - .../links/link-builder/link-creator-info.tsx | 1 - packages/ui/src/timestamp-tooltip.tsx | 23 ++++++++++++++----- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx index f9281a7525d..df503af5812 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/earnings/earnings-table.tsx @@ -83,7 +83,6 @@ export function EarningsTablePartner({ limit }: { limit?: number }) { timestamp={row.original.createdAt} side="right" rows={["local"]} - interactive > {formatDateTimeSmart(row.original.createdAt)} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx index 80f6aea2eb8..6f6b7d50bd0 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-events-columns.tsx @@ -49,7 +49,6 @@ export const campaignEventsColumns = [ timestamp={timestamp} side="top" rows={["local", "utc", "unix"]} - interactive >

{formatDateTimeSmart(row.original.createdAt)}

diff --git a/apps/web/ui/analytics/events/events-table.tsx b/apps/web/ui/analytics/events/events-table.tsx index 9d9b36896d0..bcfdd58f629 100644 --- a/apps/web/ui/analytics/events/events-table.tsx +++ b/apps/web/ui/analytics/events/events-table.tsx @@ -189,7 +189,6 @@ export default function EventsTable({ timestamp={getValue()} side="right" rows={["local", "utc", "unix"]} - interactive > {formatDateTimeSmart(getValue())} diff --git a/apps/web/ui/customers/customer-activity-list.tsx b/apps/web/ui/customers/customer-activity-list.tsx index 16d393ef6c7..5fdf629cb8a 100644 --- a/apps/web/ui/customers/customer-activity-list.tsx +++ b/apps/web/ui/customers/customer-activity-list.tsx @@ -140,7 +140,6 @@ export function CustomerActivityList({ timestamp={event.timestamp} side="right" rows={["local", "utc", "unix"]} - interactive > {formatDateTimeSmart(event.timestamp)} diff --git a/apps/web/ui/customers/customer-sales-table.tsx b/apps/web/ui/customers/customer-sales-table.tsx index 15ccb314e96..438220e644d 100644 --- a/apps/web/ui/customers/customer-sales-table.tsx +++ b/apps/web/ui/customers/customer-sales-table.tsx @@ -42,7 +42,6 @@ export function CustomerSalesTable({ timestamp={getValue()} side="right" rows={["local", "utc", "unix"]} - interactive > {formatDateTimeSmart(getValue())} diff --git a/apps/web/ui/links/link-builder/link-creator-info.tsx b/apps/web/ui/links/link-builder/link-creator-info.tsx index 6f833760f0a..986ecfb8f0f 100644 --- a/apps/web/ui/links/link-builder/link-creator-info.tsx +++ b/apps/web/ui/links/link-builder/link-creator-info.tsx @@ -32,7 +32,6 @@ export function LinkCreatorInfo({ link }: { link: ExpandedLinkProps }) { timestamp={link.createdAt} rows={["local", "utc", "unix"]} delayDuration={150} - interactive > {timeAgo(link.createdAt)} diff --git a/packages/ui/src/timestamp-tooltip.tsx b/packages/ui/src/timestamp-tooltip.tsx index d64cd3c53c0..e526945c2a0 100644 --- a/packages/ui/src/timestamp-tooltip.tsx +++ b/packages/ui/src/timestamp-tooltip.tsx @@ -28,7 +28,7 @@ function getLocalTimeZone(): string { export function TimestampTooltip({ timestamp, rows, - interactive, + interactive = true, ...tooltipProps }: TimestampTooltipProps) { if (!timestamp || new Date(timestamp).toString() === "Invalid Date") @@ -52,7 +52,7 @@ export function TimestampTooltip({ function TimestampTooltipContent({ timestamp, rows = ["local", "utc"], - interactive = false, + interactive, }: Pick) { if (!timestamp) throw new Error("Falsy timestamp not permitted in TimestampTooltipContent"); @@ -95,6 +95,7 @@ function TimestampTooltipContent({ const items: { label: string; shortLabel?: string; + successMessageLabel: string; value: string; valueMono?: boolean; }[] = useMemo( @@ -107,12 +108,14 @@ function TimestampTooltipContent({ shortLabel: new Date() .toLocaleTimeString("en-US", { timeZoneName: "short" }) .split(" ")[2], + successMessageLabel: "local timestamp", value: date.toLocaleString("en-US", commonFormat), }, utc: { label: "UTC", shortLabel: "UTC", + successMessageLabel: "UTC timestamp", value: new Date(date.getTime()).toLocaleString("en-US", { ...commonFormat, timeZone: "UTC", @@ -121,6 +124,7 @@ function TimestampTooltipContent({ unix: { label: "UNIX Timestamp", + successMessageLabel: "UNIX timestamp", value: (date.getTime() / 1000).toString(), valueMono: true, }, @@ -149,17 +153,24 @@ function TimestampTooltipContent({ key={idx} className={cn( interactive && - "before:bg-bg-emphasis relative select-none before:absolute before:-inset-x-1 before:inset-y-0 before:rounded before:opacity-0 before:content-[''] hover:cursor-pointer hover:before:opacity-60 active:before:opacity-100", + "before:bg-bg-emphasis relative select-none before:absolute before:-inset-x-1 before:inset-y-0 before:rounded before:opacity-0 before:content-[''] hover:cursor-copy hover:before:opacity-60 active:before:opacity-100", )} onClick={ interactive ? async () => { try { await navigator.clipboard.writeText(row.value); - toast.success("Copied to clipboard"); + toast.success( + `Copied ${row.successMessageLabel} to clipboard`, + ); } catch (e) { - toast.error("Failed to copy to clipboard"); - console.error("Failed to copy to clipboard", e); + toast.error( + `Failed to copy ${row.successMessageLabel} to clipboard`, + ); + console.error( + `Failed to copy ${row.successMessageLabel} to clipboard`, + e, + ); } } : undefined