From ef2d64414665538e3652b12f0369f73a2825887d Mon Sep 17 00:00:00 2001
From: Nico <47644445+nicotsx@users.noreply.github.com>
Date: Sat, 10 Jan 2026 13:02:46 +0100
Subject: [PATCH] refactor: format dates in a human-friendly way (#334)
---
app/client/components/cron-input.tsx | 8 +-
app/client/components/onoff.tsx | 2 +-
.../components/release-notes-dialog.tsx | 4 +-
app/client/components/snapshots-table.tsx | 3 +-
app/client/lib/datetime.ts | 92 +++++++++++++++++++
.../backups/components/backup-card.tsx | 9 +-
.../backups/components/schedule-summary.tsx | 9 +-
.../components/snapshot-file-browser.tsx | 3 +-
.../backups/components/snapshot-timeline.tsx | 18 ++--
.../repositories/routes/snapshot-details.tsx | 3 +-
app/client/modules/repositories/tabs/info.tsx | 7 +-
.../volumes/components/healthchecks-card.tsx | 10 +-
12 files changed, 125 insertions(+), 43 deletions(-)
create mode 100644 app/client/lib/datetime.ts
diff --git a/app/client/components/cron-input.tsx b/app/client/components/cron-input.tsx
index eb268fd7..c7e85e06 100644
--- a/app/client/components/cron-input.tsx
+++ b/app/client/components/cron-input.tsx
@@ -1,9 +1,9 @@
import { CronExpressionParser } from "cron-parser";
-import { format } from "date-fns";
import { AlertCircle, CheckCircle2 } from "lucide-react";
import { useMemo } from "react";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
import { Input } from "~/client/components/ui/input";
+import { formatDateTime } from "~/client/lib/datetime";
import { cn } from "~/client/lib/utils";
interface CronInputProps {
@@ -51,7 +51,9 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
placeholder="* * * * *"
value={value}
onChange={(e) => onChange(e.target.value)}
- className={cn("font-mono", { "border-destructive": error || (value && !isValid) })}
+ className={cn("font-mono", {
+ "border-destructive": error || (value && !isValid),
+ })}
/>
{value && (
@@ -77,7 +79,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) {
{nextRuns.map((date, i) => (
{i + 1}.
- {format(date, "PPP p")}
+ {formatDateTime(date)}
))}
diff --git a/app/client/components/onoff.tsx b/app/client/components/onoff.tsx
index 7fe95082..8161bc32 100644
--- a/app/client/components/onoff.tsx
+++ b/app/client/components/onoff.tsx
@@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors",
isOn
- ? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200"
+ ? "border-green-200 bg-green-50 text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-200"
: "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10",
)}
>
diff --git a/app/client/components/release-notes-dialog.tsx b/app/client/components/release-notes-dialog.tsx
index 81e1f765..f4f77f80 100644
--- a/app/client/components/release-notes-dialog.tsx
+++ b/app/client/components/release-notes-dialog.tsx
@@ -1,8 +1,8 @@
-import { format } from "date-fns";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
import { ScrollArea } from "~/client/components/ui/scroll-area";
+import { formatDate } from "~/client/lib/datetime";
import type { UpdateInfoDto } from "~/server/modules/system/system.dto";
interface ReleaseNotesDialogProps {
@@ -29,7 +29,7 @@ export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotes
{release.version}
- {format(new Date(release.publishedAt), "PPP")}
+ {formatDate(release.publishedAt)}
{release.body}
diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx
index 9ff31b8f..613c54eb 100644
--- a/app/client/components/snapshots-table.tsx
+++ b/app/client/components/snapshots-table.tsx
@@ -27,6 +27,7 @@ import {
} from "~/client/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
import { formatDuration } from "~/utils/utils";
+import { formatDateTime } from "~/client/lib/datetime";
import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { parseError } from "~/client/lib/errors";
import type { BackupSchedule, Snapshot } from "../lib/types";
@@ -185,7 +186,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
- {new Date(snapshot.time).toLocaleString()}
+ {formatDateTime(snapshot.time)}
diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts
new file mode 100644
index 00000000..1b107d48
--- /dev/null
+++ b/app/client/lib/datetime.ts
@@ -0,0 +1,92 @@
+import { formatDistanceToNow, isValid } from "date-fns";
+
+// 1/10/2026, 2:30 PM
+export function formatDateTime(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ return Intl.DateTimeFormat(navigator.languages, {
+ month: "numeric",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ }).format(d);
+}
+
+// Jan 10, 2026
+export function formatDateWithMonth(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+ return Intl.DateTimeFormat(navigator.languages, {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ }).format(d);
+}
+
+// 1/10/2026
+export function formatDate(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ return Intl.DateTimeFormat(navigator.languages, {
+ month: "numeric",
+ day: "numeric",
+ year: "numeric",
+ }).format(d);
+}
+
+// 1/10
+export function formatShortDate(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ return Intl.DateTimeFormat(navigator.languages, {
+ month: "numeric",
+ day: "numeric",
+ }).format(d);
+}
+
+// 1/10, 2:30 PM
+export function formatShortDateTime(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ return Intl.DateTimeFormat(navigator.languages, {
+ month: "numeric",
+ day: "numeric",
+ hour: "numeric",
+ minute: "numeric",
+ }).format(d);
+}
+
+// 2:30 PM
+export function formatTime(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ return Intl.DateTimeFormat(navigator.languages, {
+ hour: "numeric",
+ minute: "numeric",
+ }).format(d);
+}
+
+// 5 minutes ago
+export function formatTimeAgo(date: Date | string | number | null | undefined): string {
+ if (!date) return "Never";
+ const d = new Date(date);
+ if (!isValid(d)) return "Invalid Date";
+
+ const timeAgo = formatDistanceToNow(d, {
+ addSuffix: true,
+ });
+
+ return timeAgo;
+}
diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx
index 85249704..074138e0 100644
--- a/app/client/modules/backups/components/backup-card.tsx
+++ b/app/client/modules/backups/components/backup-card.tsx
@@ -3,6 +3,7 @@ import { Link } from "react-router";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import type { BackupSchedule } from "~/client/lib/types";
import { BackupStatusDot } from "./backup-status-dot";
+import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
return (
@@ -36,15 +37,11 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
Last backup
-
- {schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleDateString() : "Never"}
-
+ {formatTimeAgo(schedule.lastBackupAt)}
Next backup
-
- {schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleDateString() : "N/A"}
-
+ {formatShortDateTime(schedule.nextBackupAt)}
diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx
index 16f1df03..1801f865 100644
--- a/app/client/modules/backups/components/schedule-summary.tsx
+++ b/app/client/modules/backups/components/schedule-summary.tsx
@@ -19,6 +19,7 @@ import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { parseError } from "~/client/lib/errors";
import { Link } from "react-router";
+import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
type Props = {
schedule: BackupSchedule;
@@ -155,15 +156,11 @@ export const ScheduleSummary = (props: Props) => {
Last backup
-
- {schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleString() : "Never"}
-
+
{formatTimeAgo(schedule.lastBackupAt)}
Next backup
-
- {schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleString() : "Never"}
-
+
{formatShortDateTime(schedule.nextBackupAt)}
diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx
index 185b0ed3..e2efbb37 100644
--- a/app/client/modules/backups/components/snapshot-file-browser.tsx
+++ b/app/client/modules/backups/components/snapshot-file-browser.tsx
@@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/cli
import { Button, buttonVariants } from "~/client/components/ui/button";
import type { Snapshot } from "~/client/lib/types";
import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
+import { formatDateTime } from "~/client/lib/datetime";
import { useFileBrowser } from "~/client/hooks/use-file-browser";
import { cn } from "~/client/lib/utils";
@@ -90,7 +91,7 @@ export const SnapshotFileBrowser = (props: Props) => {
File Browser
{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}
+ >{`Viewing snapshot from ${formatDateTime(snapshot?.time)}`}
{
},
)}
>
-
- {date.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
-
-
- {date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}
-
+
{formatShortDate(date)}
+
{formatTime(date)}
@@ -98,8 +95,7 @@ export const SnapshotTimeline = (props: Props) => {
{snapshots.length} snapshots
- {new Date(snapshots[0].time).toLocaleDateString()} -{" "}
- {new Date(snapshots.at(-1)?.time ?? 0).toLocaleDateString()}
+ {formatDateWithMonth(snapshots[0].time)} - {formatDateWithMonth(snapshots.at(-1)?.time)}
diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx
index ea10fb6a..2d740d1b 100644
--- a/app/client/modules/repositories/routes/snapshot-details.tsx
+++ b/app/client/modules/repositories/routes/snapshot-details.tsx
@@ -3,6 +3,7 @@ import { redirect, useParams, Link, Await } from "react-router";
import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser";
+import { formatDateTime } from "~/client/lib/datetime";
import { getRepository, getSnapshotDetails } from "~/client/api-client";
import type { Route } from "./+types/snapshot-details";
import { Suspense } from "react";
@@ -112,7 +113,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
Time:
-
{new Date(data.snapshot.time).toLocaleString()}
+
{formatDateTime(data.snapshot.time)}
Loading...}>
diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx
index e22b8c6a..6b63330d 100644
--- a/app/client/modules/repositories/tabs/info.tsx
+++ b/app/client/modules/repositories/tabs/info.tsx
@@ -19,6 +19,7 @@ import {
} from "~/client/components/ui/alert-dialog";
import type { Repository } from "~/client/lib/types";
import { REPOSITORY_BASE } from "~/client/lib/constants";
+import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime";
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
@@ -131,13 +132,11 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
)}
Created at
-
{new Date(repository.createdAt).toLocaleString()}
+
{formatDateTime(repository.createdAt)}
Last checked
-
- {repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"}
-
+
{formatTimeAgo(repository.lastChecked)}
{config.cacert && (
diff --git a/app/client/modules/volumes/components/healthchecks-card.tsx b/app/client/modules/volumes/components/healthchecks-card.tsx
index 198efc46..86a3c88e 100644
--- a/app/client/modules/volumes/components/healthchecks-card.tsx
+++ b/app/client/modules/volumes/components/healthchecks-card.tsx
@@ -1,11 +1,11 @@
import { useMutation } from "@tanstack/react-query";
-import { formatDistanceToNow } from "date-fns";
import { Activity, HeartIcon } from "lucide-react";
import { toast } from "sonner";
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { OnOff } from "~/client/components/onoff";
import { Button } from "~/client/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
+import { formatTimeAgo } from "~/client/lib/datetime";
import type { Volume } from "~/client/lib/types";
type Props = {
@@ -13,10 +13,6 @@ type Props = {
};
export const HealthchecksCard = ({ volume }: Props) => {
- const timeAgo = formatDistanceToNow(volume.lastHealthCheck, {
- addSuffix: true,
- });
-
const healthcheck = useMutation({
...healthCheckVolumeMutation(),
onSuccess: (d) => {
@@ -55,9 +51,9 @@ export const HealthchecksCard = ({ volume }: Props) => {
{volume.lastError && {volume.lastError}}
- {volume.status === "mounted" && Healthy}
+ {volume.status === "mounted" && Healthy}
{volume.status !== "unmounted" && (
- Checked {timeAgo || "never"}
+ Checked {formatTimeAgo(volume.lastHealthCheck)}
)}
Remount on error