From 45ae281a6c1f9c170e92437c11f68eed33aae415 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Mon, 23 Mar 2026 20:57:02 +0100 Subject: [PATCH] refactor(client): configure time formatting with timezone from server --- app/client/components/cron-input.tsx | 3 +- .../file-browsers/local-file-browser.tsx | 3 +- .../file-browsers/snapshot-tree-browser.tsx | 15 +++-- .../file-browsers/volume-file-browser.tsx | 15 +++-- app/client/components/file-tree.tsx | 8 ++- .../components/release-notes-dialog.tsx | 4 +- app/client/components/snapshots-table.tsx | 3 +- app/client/components/time-ago.tsx | 12 ++++ app/client/lib/__tests__/datetime.test.ts | 4 ++ app/client/lib/datetime.ts | 61 ++++++++++++++----- .../backups/components/backup-card.tsx | 9 ++- .../backups/components/schedule-summary.tsx | 8 ++- .../components/snapshot-file-browser.tsx | 3 +- .../backups/components/snapshot-timeline.tsx | 3 +- .../repositories/components/doctor-report.tsx | 4 +- .../repositories/routes/snapshot-details.tsx | 3 +- app/client/modules/repositories/tabs/info.tsx | 6 +- .../sso/components/sso-settings-section.tsx | 3 +- .../volumes/components/healthchecks-card.tsx | 10 +-- app/client/modules/volumes/tabs/info.tsx | 4 +- app/routes/__root.tsx | 15 ++++- 21 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 app/client/components/time-ago.tsx diff --git a/app/client/components/cron-input.tsx b/app/client/components/cron-input.tsx index 35e387ed..fde40eec 100644 --- a/app/client/components/cron-input.tsx +++ b/app/client/components/cron-input.tsx @@ -3,7 +3,7 @@ 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 { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; interface CronInputProps { @@ -13,6 +13,7 @@ interface CronInputProps { } export function CronInput({ value, onChange, error }: CronInputProps) { + const { formatDateTime } = useTimeFormat(); const { isValid, nextRuns, parseError } = useMemo(() => { if (!value) { return { isValid: false, nextRuns: [], parseError: null }; diff --git a/app/client/components/file-browsers/local-file-browser.tsx b/app/client/components/file-browsers/local-file-browser.tsx index b2213626..69e8d010 100644 --- a/app/client/components/file-browsers/local-file-browser.tsx +++ b/app/client/components/file-browsers/local-file-browser.tsx @@ -4,6 +4,7 @@ import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-b import { useFileBrowser } from "~/client/hooks/use-file-browser"; import { parseError } from "~/client/lib/errors"; import { normalizeAbsolutePath } from "@zerobyte/core/utils"; +import { logger } from "~/client/lib/logger"; type LocalFileBrowserProps = FileBrowserUiProps & { initialPath?: string; @@ -26,7 +27,7 @@ export const LocalFileBrowser = ({ initialPath = "/", enabled = true, ...uiProps return await queryClient.ensureQueryData(browseFilesystemOptions({ query: { path } })); }, prefetchFolder: (path) => { - void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } })); + void queryClient.prefetchQuery(browseFilesystemOptions({ query: { path } })).catch((e) => logger.error(e)); }, }); diff --git a/app/client/components/file-browsers/snapshot-tree-browser.tsx b/app/client/components/file-browsers/snapshot-tree-browser.tsx index 507b0002..993b935f 100644 --- a/app/client/components/file-browsers/snapshot-tree-browser.tsx +++ b/app/client/components/file-browsers/snapshot-tree-browser.tsx @@ -5,6 +5,7 @@ import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-b import { useFileBrowser } from "~/client/hooks/use-file-browser"; import { parseError } from "~/client/lib/errors"; import { normalizeAbsolutePath } from "@zerobyte/core/utils"; +import { logger } from "~/client/lib/logger"; type SnapshotTreeBrowserProps = FileBrowserUiProps & { repositoryId: string; @@ -79,12 +80,14 @@ export const SnapshotTreeBrowser = ({ ); }, prefetchFolder: (path) => { - void queryClient.prefetchQuery( - listSnapshotFilesOptions({ - path: { shortId: repositoryId, snapshotId }, - query: { path, offset: 0, limit: pageSize }, - }), - ); + void queryClient + .prefetchQuery( + listSnapshotFilesOptions({ + path: { shortId: repositoryId, snapshotId }, + query: { path, offset: 0, limit: pageSize }, + }), + ) + .catch((e) => logger.error(e)); }, pathTransform: { strip: stripBasePath, diff --git a/app/client/components/file-browsers/volume-file-browser.tsx b/app/client/components/file-browsers/volume-file-browser.tsx index 902b191a..347da3ef 100644 --- a/app/client/components/file-browsers/volume-file-browser.tsx +++ b/app/client/components/file-browsers/volume-file-browser.tsx @@ -3,6 +3,7 @@ import { listFilesOptions } from "~/client/api-client/@tanstack/react-query.gen" import { FileBrowser, type FileBrowserUiProps } from "~/client/components/file-browsers/file-browser"; import { useFileBrowser, type FetchFolderResult } from "~/client/hooks/use-file-browser"; import { parseError } from "~/client/lib/errors"; +import { logger } from "~/client/lib/logger"; type VolumeFileBrowserProps = FileBrowserUiProps & { volumeId: string; @@ -29,12 +30,14 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu ); }, prefetchFolder: (path) => { - void queryClient.prefetchQuery( - listFilesOptions({ - path: { shortId: volumeId }, - query: { path }, - }), - ); + void queryClient + .prefetchQuery( + listFilesOptions({ + path: { shortId: volumeId }, + query: { path }, + }), + ) + .catch((e) => logger.error(e)); }, }); diff --git a/app/client/components/file-tree.tsx b/app/client/components/file-tree.tsx index d9342f73..58e18b82 100644 --- a/app/client/components/file-tree.tsx +++ b/app/client/components/file-tree.tsx @@ -581,8 +581,10 @@ const NodeButton = memo(({ depth, icon, onClick, onMouseEnter, className, childr ); return ( - + ); }); diff --git a/app/client/components/release-notes-dialog.tsx b/app/client/components/release-notes-dialog.tsx index f4f77f80..77ea9289 100644 --- a/app/client/components/release-notes-dialog.tsx +++ b/app/client/components/release-notes-dialog.tsx @@ -2,7 +2,7 @@ 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 { useTimeFormat } from "~/client/lib/datetime"; import type { UpdateInfoDto } from "~/server/modules/system/system.dto"; interface ReleaseNotesDialogProps { @@ -12,6 +12,8 @@ interface ReleaseNotesDialogProps { } export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotesDialogProps) { + const { formatDate } = useTimeFormat(); + if (!updates) return null; return ( diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 275a8a79..56bb00c9 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -25,8 +25,8 @@ import { DialogTitle, } from "~/client/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { useTimeFormat } from "~/client/lib/datetime"; import { formatDuration } from "~/utils/utils"; -import { formatDateTime } from "~/client/lib/datetime"; import { deleteSnapshotsMutation, listSnapshotsQueryKey, @@ -49,6 +49,7 @@ type Props = { export const SnapshotsTable = ({ snapshots, repositoryId, backups, listSnapshotsQueryOptions }: Props) => { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { formatDateTime } = useTimeFormat(); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false); diff --git a/app/client/components/time-ago.tsx b/app/client/components/time-ago.tsx new file mode 100644 index 00000000..94c93a74 --- /dev/null +++ b/app/client/components/time-ago.tsx @@ -0,0 +1,12 @@ +import { useTimeFormat, type DateInput } from "~/client/lib/datetime"; + +type Props = { + date: DateInput; + className?: string; +}; + +export function TimeAgo({ date, className }: Props) { + const { formatTimeAgo } = useTimeFormat(); + + return {formatTimeAgo(date)}; +} diff --git a/app/client/lib/__tests__/datetime.test.ts b/app/client/lib/__tests__/datetime.test.ts index 4e2475f4..544a9aeb 100644 --- a/app/client/lib/__tests__/datetime.test.ts +++ b/app/client/lib/__tests__/datetime.test.ts @@ -60,4 +60,8 @@ describe("datetime formatters", () => { nowSpy.mockRestore(); }); + + test("formats calendar values with an explicit locale and timezone", () => { + expect(formatShortDateTime(sampleDate, { locale: "en-US", timeZone: "UTC" })).toBe("1/10, 2:30 PM"); + }); }); diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts index 587544ad..2a7ada92 100644 --- a/app/client/lib/datetime.ts +++ b/app/client/lib/datetime.ts @@ -1,8 +1,13 @@ import { formatDistanceToNow, isValid } from "date-fns"; +import { useMemo } from "react"; +import { Route as RootRoute } from "~/routes/__root"; -type DateInput = Date | string | number | null | undefined; +export type DateInput = Date | string | number | null | undefined; -const getLocales = () => (typeof navigator !== "undefined" ? navigator.languages : undefined); +type DateFormatOptions = { + locale?: string | string[]; + timeZone?: string; +}; function formatValidDate(date: DateInput, formatter: (date: Date) => string): string { if (!date) return "Never"; @@ -13,10 +18,21 @@ function formatValidDate(date: DateInput, formatter: (date: Date) => string): st return formatter(parsedDate); } +function getDateTimeFormat( + locale: DateFormatOptions["locale"], + timeZone: DateFormatOptions["timeZone"], + options: Intl.DateTimeFormatOptions, +) { + return Intl.DateTimeFormat(locale, { + ...options, + timeZone, + }); +} + // 1/10/2026, 2:30 PM -export function formatDateTime(date: DateInput): string { +export function formatDateTime(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { month: "numeric", day: "numeric", year: "numeric", @@ -27,9 +43,9 @@ export function formatDateTime(date: DateInput): string { } // Jan 10, 2026 -export function formatDateWithMonth(date: DateInput): string { +export function formatDateWithMonth(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { month: "short", day: "numeric", year: "numeric", @@ -38,9 +54,9 @@ export function formatDateWithMonth(date: DateInput): string { } // 1/10/2026 -export function formatDate(date: DateInput): string { +export function formatDate(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { month: "numeric", day: "numeric", year: "numeric", @@ -49,9 +65,9 @@ export function formatDate(date: DateInput): string { } // 1/10 -export function formatShortDate(date: DateInput): string { +export function formatShortDate(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { month: "numeric", day: "numeric", }).format(validDate), @@ -59,9 +75,9 @@ export function formatShortDate(date: DateInput): string { } // 1/10, 2:30 PM -export function formatShortDateTime(date: DateInput): string { +export function formatShortDateTime(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { month: "numeric", day: "numeric", hour: "numeric", @@ -71,9 +87,9 @@ export function formatShortDateTime(date: DateInput): string { } // 2:30 PM -export function formatTime(date: DateInput): string { +export function formatTime(date: DateInput, options: DateFormatOptions = {}): string { return formatValidDate(date, (validDate) => - Intl.DateTimeFormat(getLocales(), { + getDateTimeFormat(options.locale, options.timeZone, { hour: "numeric", minute: "numeric", }).format(validDate), @@ -91,3 +107,20 @@ export function formatTimeAgo(date: DateInput): string { return timeAgo.replace("about ", "").replace("over ", "").replace("almost ", "").replace("less than ", ""); }); } + +export function useTimeFormat() { + const { locale, timeZone } = RootRoute.useLoaderData(); + + return useMemo( + () => ({ + formatDateTime: (date: DateInput) => formatDateTime(date, { locale, timeZone }), + formatDateWithMonth: (date: DateInput) => formatDateWithMonth(date, { locale, timeZone }), + formatDate: (date: DateInput) => formatDate(date, { locale, timeZone }), + formatShortDate: (date: DateInput) => formatShortDate(date, { locale, timeZone }), + formatShortDateTime: (date: DateInput) => formatShortDateTime(date, { locale, timeZone }), + formatTime: (date: DateInput) => formatTime(date, { locale, timeZone }), + formatTimeAgo, + }), + [locale, timeZone], + ); +} diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 9b8eec13..49b7db63 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -1,11 +1,14 @@ import { CalendarClock, Database, HardDrive } from "lucide-react"; +import { Link } from "@tanstack/react-router"; +import { TimeAgo } from "~/client/components/time-ago"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { useTimeFormat } from "~/client/lib/datetime"; import type { BackupSchedule } from "~/client/lib/types"; import { BackupStatusDot } from "./backup-status-dot"; -import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; -import { Link } from "@tanstack/react-router"; export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { + const { formatShortDateTime } = useTimeFormat(); + return ( @@ -42,7 +45,7 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
Last backup
- {formatTimeAgo(schedule.lastBackupAt)} +
Next backup diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx index 089b6c5f..dec510d5 100644 --- a/app/client/modules/backups/components/schedule-summary.tsx +++ b/app/client/modules/backups/components/schedule-summary.tsx @@ -16,11 +16,12 @@ import type { BackupSchedule } from "~/client/lib/types"; import { BackupProgressCard } from "./backup-progress-card"; import { getBackupProgressOptions, runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { Link } from "@tanstack/react-router"; import { toast } from "sonner"; import { handleRepositoryError } from "~/client/lib/errors"; -import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; -import { Link } from "@tanstack/react-router"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible"; +import { TimeAgo } from "~/client/components/time-ago"; +import { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; type Props = { @@ -35,6 +36,7 @@ type Props = { export const ScheduleSummary = (props: Props) => { const { schedule, handleToggleEnabled, handleRunBackupNow, handleStopBackup, handleDeleteSchedule, setIsEditMode } = props; + const { formatShortDateTime } = useTimeFormat(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showForgetConfirm, setShowForgetConfirm] = useState(false); const [showStopConfirm, setShowStopConfirm] = useState(false); @@ -182,7 +184,7 @@ export const ScheduleSummary = (props: Props) => {

Last backup

-

{formatTimeAgo(schedule.lastBackupAt)}

+

Next backup

diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 3bdda3aa..bbe28f8b 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Button, buttonVariants } from "~/client/components/ui/button"; import type { Snapshot } from "~/client/lib/types"; -import { formatDateTime } from "~/client/lib/datetime"; +import { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; import { Link } from "@tanstack/react-router"; import { SnapshotTreeBrowser } from "~/client/components/file-browsers/snapshot-tree-browser"; @@ -62,6 +62,7 @@ const TreeBrowserFallback = () => ( export const SnapshotFileBrowser = (props: Props) => { const { snapshot, repositoryId, backupId, basePath, onDeleteSnapshot, isDeletingSnapshot } = props; + const { formatDateTime } = useTimeFormat(); const scheduleShortId = !basePath ? backupId || snapshot.tags?.[0] : undefined; diff --git a/app/client/modules/backups/components/snapshot-timeline.tsx b/app/client/modules/backups/components/snapshot-timeline.tsx index 40489e56..5b74b37d 100644 --- a/app/client/modules/backups/components/snapshot-timeline.tsx +++ b/app/client/modules/backups/components/snapshot-timeline.tsx @@ -4,7 +4,7 @@ import type { ListSnapshotsResponse } from "~/client/api-client"; import { ByteSize } from "~/client/components/bytes-size"; import { Card, CardContent } from "~/client/components/ui/card"; import { Button } from "~/client/components/ui/button"; -import { formatDateWithMonth, formatShortDate, formatTime } from "~/client/lib/datetime"; +import { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; import { RetentionCategoryBadges } from "~/client/components/retention-category-badges"; @@ -45,6 +45,7 @@ interface Props { export const SnapshotTimeline = (props: Props) => { const { snapshots, snapshotId, loading, onSnapshotSelect, error, initialSortOrder = "asc" } = props; const selectedRef = useRef(null); + const { formatDateWithMonth, formatShortDate, formatTime } = useTimeFormat(); const [sortOrder, setSortOrder] = useState(initialSortOrder); const sortedSnapshots = useMemo(() => getSortedSnapshots(snapshots, sortOrder), [snapshots, sortOrder]); const snapshotRange = useMemo(() => getSnapshotRange(snapshots), [snapshots]); diff --git a/app/client/modules/repositories/components/doctor-report.tsx b/app/client/modules/repositories/components/doctor-report.tsx index 3cb9e7a5..1c2dde65 100644 --- a/app/client/modules/repositories/components/doctor-report.tsx +++ b/app/client/modules/repositories/components/doctor-report.tsx @@ -1,6 +1,6 @@ import { AlertCircle, CheckCircle2 } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible"; -import { formatDateTime } from "~/client/lib/datetime"; +import { useTimeFormat } from "~/client/lib/datetime"; import { cn, safeJsonParse } from "~/client/lib/utils"; import { Card, CardTitle } from "~/client/components/ui/card"; @@ -23,6 +23,8 @@ type Props = { }; export const DoctorReport = ({ result, repositoryStatus }: Props) => { + const { formatDateTime } = useTimeFormat(); + return (
diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index 1d1d14c5..43a6f301 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -8,7 +8,7 @@ import type { GetSnapshotDetailsResponse } from "~/client/api-client/types.gen"; import { Button } from "~/client/components/ui/button"; 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 { useTimeFormat } from "~/client/lib/datetime"; import { BackupSummaryCard } from "~/client/components/backup-summary-card"; import { useState } from "react"; import { Database } from "lucide-react"; @@ -58,6 +58,7 @@ type Props = { export function SnapshotDetailsPage({ repositoryId, snapshotId, initialSnapshot }: Props) { const [showAllPaths, setShowAllPaths] = useState(false); + const { formatDateTime } = useTimeFormat(); const { data: repository } = useSuspenseQuery({ ...getRepositoryOptions({ path: { shortId: repositoryId } }), diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index 6b73e69b..fdaae655 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -15,7 +15,6 @@ import { } from "~/client/components/ui/alert-dialog"; import type { Repository } from "~/client/lib/types"; import type { GetRepositoryStatsResponse } from "~/client/api-client/types.gen"; -import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; import { cancelDoctorMutation, deleteRepositoryMutation, @@ -23,6 +22,8 @@ import { unlockRepositoryMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import type { RepositoryConfig } from "@zerobyte/core/restic"; +import { TimeAgo } from "~/client/components/time-ago"; +import { useTimeFormat } from "~/client/lib/datetime"; import { DoctorReport } from "../components/doctor-report"; import { parseError } from "~/client/lib/errors"; import { useNavigate } from "@tanstack/react-router"; @@ -41,6 +42,7 @@ const getEffectiveLocalPath = (repository: Repository): string | null => { }; export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) => { + const { formatDateTime } = useTimeFormat(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const navigate = useNavigate(); @@ -206,7 +208,7 @@ export const RepositoryInfoTabContent = ({ repository, initialStats }: Props) =>
Last Checked
-

{formatTimeAgo(repository.lastChecked)}

+
{hasLocalPath && (
diff --git a/app/client/modules/sso/components/sso-settings-section.tsx b/app/client/modules/sso/components/sso-settings-section.tsx index 6b5adcad..ca424247 100644 --- a/app/client/modules/sso/components/sso-settings-section.tsx +++ b/app/client/modules/sso/components/sso-settings-section.tsx @@ -29,7 +29,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~ import { Switch } from "~/client/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; import { useOrganizationContext } from "~/client/hooks/use-org-context"; -import { formatDateWithMonth } from "~/client/lib/datetime"; +import { useTimeFormat } from "~/client/lib/datetime"; import { getOrigin } from "~/client/functions/get-origin"; import { authClient } from "~/client/lib/auth-client"; import { cn } from "~/client/lib/utils"; @@ -47,6 +47,7 @@ export function SsoSettingsSection({ initialSettings, initialOrigin }: Props) { const { data } = useSuspenseQuery({ queryKey: ["app-origin"], queryFn: originQuery, initialData: initialOrigin }); const navigate = useNavigate(); + const { formatDateWithMonth } = useTimeFormat(); const { activeOrganization } = useOrganizationContext(); const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("member"); diff --git a/app/client/modules/volumes/components/healthchecks-card.tsx b/app/client/modules/volumes/components/healthchecks-card.tsx index 36ead717..de3f6ad2 100644 --- a/app/client/modules/volumes/components/healthchecks-card.tsx +++ b/app/client/modules/volumes/components/healthchecks-card.tsx @@ -5,8 +5,8 @@ import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-cl 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"; +import { TimeAgo } from "~/client/components/time-ago"; type Props = { volume: Volume; @@ -53,9 +53,11 @@ export const HealthchecksCard = ({ volume }: Props) => { {volume.lastError && {volume.lastError}} {volume.status === "mounted" && Healthy} {volume.status !== "unmounted" && ( - Checked {formatTimeAgo(volume.lastHealthCheck)} + + Checked + )} - +
Remount on error { enabledLabel="Enabled" disabledLabel="Paused" /> - +
{volume.status !== "unmounted" && (
diff --git a/app/client/modules/volumes/tabs/info.tsx b/app/client/modules/volumes/tabs/info.tsx index b6bebcc1..d01dd98e 100644 --- a/app/client/modules/volumes/tabs/info.tsx +++ b/app/client/modules/volumes/tabs/info.tsx @@ -81,9 +81,9 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => { const deleteVol = useMutation({ ...deleteVolumeMutation(), - onSuccess: () => { + onSuccess: async () => { toast.success("Volume deleted successfully"); - void navigate({ to: "/volumes" }); + await navigate({ to: "/volumes" }); }, onError: (error) => { toast.error("Failed to delete volume", { diff --git a/app/routes/__root.tsx b/app/routes/__root.tsx index 7938de8c..d95d4cdb 100644 --- a/app/routes/__root.tsx +++ b/app/routes/__root.tsx @@ -8,7 +8,7 @@ import { useServerEvents } from "~/client/hooks/use-server-events"; import { useEffect } from "react"; import { ThemeProvider, THEME_COOKIE_NAME } from "~/client/components/theme-provider"; import { createServerFn } from "@tanstack/react-start"; -import { getCookie } from "@tanstack/react-start/server"; +import { getCookie, getRequestHeaders } from "@tanstack/react-start/server"; import { isAuthRoute } from "~/lib/auth-routes"; const fetchTheme = createServerFn({ method: "GET" }).handler(async () => { @@ -16,13 +16,22 @@ const fetchTheme = createServerFn({ method: "GET" }).handler(async () => { return (themeCookie === "light" ? "light" : "dark") as "light" | "dark"; }); +const fetchTimeConfig = createServerFn({ method: "GET" }).handler(async () => { + const acceptLanguage = getRequestHeaders().get("accept-language"); + + return { + locale: (acceptLanguage?.split(",")[0] || "en-US") as string, + timeZone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", + }; +}); + export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ server: { middleware: [apiClientMiddleware], }, loader: async () => { - const theme = await fetchTheme(); - return { theme }; + const [theme, timeConfig] = await Promise.all([fetchTheme(), fetchTimeConfig()]); + return { theme, ...timeConfig }; }, head: () => ({ meta: [{ title: "Zerobyte - Open Source Backup Solution" }],