From 1507cf885134e93dbb0ca6f08f62ff759274f5d8 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Wed, 19 Aug 2026 11:32:56 +0200 Subject: [PATCH 01/17] replace date-fns usage with Intl.RelativeTimeFormat --- .../device-detail/device-detail-box.tsx | 18 ++++--------- app/components/device-detail/graph.tsx | 1 - app/lib/date.ts | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 app/lib/date.ts diff --git a/app/components/device-detail/device-detail-box.tsx b/app/components/device-detail/device-detail-box.tsx index f4a90404..fd575f83 100644 --- a/app/components/device-detail/device-detail-box.tsx +++ b/app/components/device-detail/device-detail-box.tsx @@ -1,6 +1,4 @@ import clsx from 'clsx' -import { formatDistanceToNow } from 'date-fns' -import { de, enUS } from 'date-fns/locale' import { ChevronUp, Minus, @@ -81,6 +79,7 @@ import { useGlobalCompareMode } from './useGlobalCompareMode' import { type SensorWithLatestMeasurement } from '~/db/schema' import { getArchiveLink } from '~/lib/archive-link' import { type loader } from '~/routes/explore.$deviceId' +import { dateDiffToNowInWords } from '~/lib/date' export interface MeasurementProps { sensorId: string @@ -96,7 +95,6 @@ export default function DeviceDetailBox() { const matches = useMatches() const { toast } = useToast() const { t, i18n } = useTranslation('device-detail-box') - const dateLocale = i18n.language.startsWith('de') ? de : enUS const dateFormatter = new Intl.DateTimeFormat(i18n.resolvedLanguage, { year: 'numeric', month: 'long', @@ -569,15 +567,12 @@ export default function DeviceDetailBox() { >

{sensor.lastMeasurement - ? formatDistanceToNow( + ? dateDiffToNowInWords( + i18n.language, new Date( sensor.lastMeasurement .createdAt, ), - { - addSuffix: true, - locale: dateLocale, - }, ) : t('no_recent_data')}

@@ -656,15 +651,12 @@ export default function DeviceDetailBox() { >

{sensor.lastMeasurement - ? formatDistanceToNow( + ? dateDiffToNowInWords( + i18n.language, new Date( sensor.lastMeasurement .createdAt, ), - { - addSuffix: true, - locale: dateLocale, - }, ) : t('no_recent_data')}

diff --git a/app/components/device-detail/graph.tsx b/app/components/device-detail/graph.tsx index 2186a98f..5df5829e 100644 --- a/app/components/device-detail/graph.tsx +++ b/app/components/device-detail/graph.tsx @@ -11,7 +11,6 @@ import { type ChartOptions, } from 'chart.js' import 'chartjs-adapter-date-fns' -// import { de, enGB } from "date-fns/locale"; import { Download, RefreshCcw, X } from 'lucide-react' import { useMemo, diff --git a/app/lib/date.ts b/app/lib/date.ts new file mode 100644 index 00000000..370cbf69 --- /dev/null +++ b/app/lib/date.ts @@ -0,0 +1,27 @@ +const ONE_MINUTE_IN_S = 60 +const ONE_HOUR_IN_S = 60 * ONE_MINUTE_IN_S +const ONE_DAY_IN_S = 24 * ONE_HOUR_IN_S +const ONE_WEEK_IN_S = 7 * ONE_DAY_IN_S +const ONE_MONTH_IN_S = 4 * ONE_WEEK_IN_S +const ONE_QUARTER_IN_S = 3 * ONE_MONTH_IN_S +const ONE_YEAR_IN_S = 12 * ONE_MONTH_IN_S + +export const dateDiffToNowInWords = (locale: string, date: Date) => { + const r = new Intl.RelativeTimeFormat(locale) + const now = new Date() + const diffInSeconds = Math.round((now.getTime() - date.getTime()) / 1000) + + if (diffInSeconds < ONE_MINUTE_IN_S) return r.format(-diffInSeconds, 'second') + if (diffInSeconds < ONE_HOUR_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_MINUTE_IN_S), 'minute') + if (diffInSeconds < ONE_DAY_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_HOUR_IN_S), 'hour') + if (diffInSeconds < ONE_WEEK_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_DAY_IN_S), 'day') + if (diffInSeconds < ONE_MONTH_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_WEEK_IN_S), 'week') + if (diffInSeconds < ONE_QUARTER_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'week') + if (diffInSeconds < ONE_YEAR_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') +} From 4205b562a7b4620203553e8dec4421b45df7766c Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Wed, 19 Aug 2026 12:01:36 +0200 Subject: [PATCH 02/17] remove date-fns from range-picker --- app/components/ui/range-picker.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/components/ui/range-picker.tsx b/app/components/ui/range-picker.tsx index a7f9d939..d1b9a325 100644 --- a/app/components/ui/range-picker.tsx +++ b/app/components/ui/range-picker.tsx @@ -1,7 +1,6 @@ 'use client' import * as React from 'react' -import { addDays, format } from 'date-fns' import { CalendarIcon } from 'lucide-react' import { type DateRange } from '@daypicker/react' @@ -13,11 +12,19 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover' +import { useTranslation } from 'react-i18next' export function DatePickerWithRange() { + const { i18n } = useTranslation() const [date, setDate] = React.useState({ from: new Date(new Date().getFullYear(), 0, 20), - to: addDays(new Date(new Date().getFullYear(), 0, 20), 20), + to: new Date(new Date().getFullYear(), 1, 10), + }) + + const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { + year: 'numeric', + month: 'long', + day: 'numeric', }) return ( @@ -34,11 +41,11 @@ export function DatePickerWithRange() { {date?.from ? ( date.to ? ( <> - {format(date.from, 'LLL dd, y')} -{' '} - {format(date.to, 'LLL dd, y')} + {dateTimeFormat.format(date.from)} -{' '} + {dateTimeFormat.format(date.to)} ) : ( - format(date.from, 'LLL dd, y') + dateTimeFormat.format(date.from) ) ) : ( Pick a date From e92ae2b8beeb4a46676e56a51c31ad19f6b4a738 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Wed, 19 Aug 2026 12:03:44 +0200 Subject: [PATCH 03/17] remove date-fns from mobile overview layer --- .../map/layers/mobile/mobile-overview-layer.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index 6aa8caf4..ebc5089a 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -1,6 +1,5 @@ import bbox from '@turf/bbox' import { point, featureCollection } from '@turf/helpers' -import { format } from 'date-fns' import { type FeatureCollection, type Point } from 'geojson' import { CalendarClock } from 'lucide-react' import { useState, useEffect, useMemo, useCallback } from 'react' @@ -10,6 +9,7 @@ import { type LocationPoint, categorizeIntoTrips, } from '~/lib/mobile-box-helper' +import { useTranslation } from 'react-i18next' const FIT_PADDING = 100 @@ -121,6 +121,9 @@ export default function MobileOverviewLayer({ }: { locations: LocationPoint[] }) { + const { i18n } = useTranslation() + const dateTimeFormat = new Intl.DateTimeFormat(i18n.language) + // Generate trips and assign colors once const trips = useMemo(() => categorizeIntoTrips(locations, 50), [locations]) @@ -451,7 +454,7 @@ export default function MobileOverviewLayer({ )}

- {format(new Date(popupInfo.startTime), 'Pp')} + {dateTimeFormat.format(new Date(popupInfo.startTime))}

{popupInfo.isCluster && @@ -461,7 +464,7 @@ export default function MobileOverviewLayer({ To

- {format(new Date(popupInfo.endTime), 'Pp')} + {dateTimeFormat.format(new Date(popupInfo.endTime))}

)} From 2ae3c26079a0c8605111956c744a0a3df313c136 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Wed, 19 Aug 2026 12:18:35 +0200 Subject: [PATCH 04/17] process review --- app/components/map/layers/mobile/mobile-overview-layer.tsx | 5 ++++- app/lib/date.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index ebc5089a..ecc2d832 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -122,7 +122,10 @@ export default function MobileOverviewLayer({ locations: LocationPoint[] }) { const { i18n } = useTranslation() - const dateTimeFormat = new Intl.DateTimeFormat(i18n.language) + const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { + hour: 'numeric', + minute: '2-digit', + }) // Generate trips and assign colors once const trips = useMemo(() => categorizeIntoTrips(locations, 50), [locations]) diff --git a/app/lib/date.ts b/app/lib/date.ts index 370cbf69..89d6e085 100644 --- a/app/lib/date.ts +++ b/app/lib/date.ts @@ -21,7 +21,7 @@ export const dateDiffToNowInWords = (locale: string, date: Date) => { if (diffInSeconds < ONE_MONTH_IN_S) return r.format(-Math.round(diffInSeconds / ONE_WEEK_IN_S), 'week') if (diffInSeconds < ONE_QUARTER_IN_S) - return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'week') + return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'quarter') if (diffInSeconds < ONE_YEAR_IN_S) return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') } From 29cba82d34b3ab872aca93c5b83e0cafcaf183a0 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Thu, 20 Aug 2026 10:02:11 +0200 Subject: [PATCH 05/17] make sure the date is correctly translated --- app/components/mydevices/dt/columns.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index f3978eb5..1f27f07f 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -32,8 +32,11 @@ export function getColumns( useTranslation: UseTranslationResponse<'data-table', any>, opts?: { isOwner?: boolean }, ): ColumnDef[] { - const { t } = useTranslation + const { t, i18n } = useTranslation const isOwner = opts?.isOwner ?? false + const dateTimeFormat = Intl.DateTimeFormat(i18n.language, { + dateStyle: 'short', + }) return [ { accessorKey: 'name', @@ -90,7 +93,7 @@ export function getColumns( }, cell: ({ row }) => { const date = new Date(row.getValue('createdAt')) - return
{date.toLocaleDateString()}
+ return
{dateTimeFormat.format(date)}
}, }, { From 8c666be863a3cf5e3a361bd7d1a997215d5ce5a1 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Thu, 20 Aug 2026 10:06:51 +0200 Subject: [PATCH 06/17] use a simpler approach for date time formatting --- app/components/mydevices/dt/columns.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index 1f27f07f..3eafa5f5 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -34,9 +34,6 @@ export function getColumns( ): ColumnDef[] { const { t, i18n } = useTranslation const isOwner = opts?.isOwner ?? false - const dateTimeFormat = Intl.DateTimeFormat(i18n.language, { - dateStyle: 'short', - }) return [ { accessorKey: 'name', @@ -93,7 +90,7 @@ export function getColumns( }, cell: ({ row }) => { const date = new Date(row.getValue('createdAt')) - return
{dateTimeFormat.format(date)}
+ return
{date.toLocaleDateString(i18n.language)}
}, }, { From dabf422f251cb86fb3f467c41a5e4891f6c2622c Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Thu, 20 Aug 2026 10:10:07 +0200 Subject: [PATCH 07/17] add i18n.lnaguage to missing calls to toLocaleString() --- app/routes/admin.devices._index.tsx | 4 +++- app/routes/admin.users._index.tsx | 6 ++++-- app/routes/device.$deviceId.edit.logs.tsx | 6 ++++-- app/routes/device.$deviceId.edit.transfer.tsx | 4 ++-- app/routes/profile.$username.tsx | 4 ++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/routes/admin.devices._index.tsx b/app/routes/admin.devices._index.tsx index 64d2e9ba..47ed551b 100644 --- a/app/routes/admin.devices._index.tsx +++ b/app/routes/admin.devices._index.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router' import { type Route } from './+types/admin.devices._index' import { getDevices } from '~/db/models/device.server' +import { useTranslation } from 'react-i18next' export async function loader({}: Route.LoaderArgs) { const devices = await getDevices('json') @@ -11,6 +12,7 @@ export default function AdminDevicesIndexRoute({ loaderData, }: Route.ComponentProps) { const { devices } = loaderData + const { i18n } = useTranslation() return (
@@ -43,7 +45,7 @@ export default function AdminDevicesIndexRoute({ {device.status} - {new Date(device.createdAt).toLocaleString()} + {new Date(device.createdAt).toLocaleString(i18n.language)} @@ -45,10 +47,10 @@ export default function AdminUsersIndexRoute({ {user.role} - {new Date(user.createdAt).toLocaleString()} + {new Date(user.createdAt).toLocaleString(i18n.language)} - {new Date(user.updatedAt).toLocaleString()} + {new Date(user.updatedAt).toLocaleString(i18n.language)} {/* {user.devicesCount} diff --git a/app/routes/device.$deviceId.edit.logs.tsx b/app/routes/device.$deviceId.edit.logs.tsx index 03a32f13..720c76cd 100644 --- a/app/routes/device.$deviceId.edit.logs.tsx +++ b/app/routes/device.$deviceId.edit.logs.tsx @@ -94,7 +94,7 @@ export default function Logs() { const { logEntries } = useLoaderData() const actionData = useActionData() const { toast } = useToast() - const { t } = useTranslation('edit-device-logs') + const { t, i18n } = useTranslation('edit-device-logs') const [newLogContent, setNewLogContent] = useState('') const submit = useSubmit() @@ -169,7 +169,9 @@ export default function Logs() { {logEntry.content} - {new Date(logEntry.createdAt).toLocaleString()} + {new Date(logEntry.createdAt).toLocaleString( + i18n.language, + )}
diff --git a/app/routes/device.$deviceId.edit.transfer.tsx b/app/routes/device.$deviceId.edit.transfer.tsx index 82e57c3f..8566397f 100644 --- a/app/routes/device.$deviceId.edit.transfer.tsx +++ b/app/routes/device.$deviceId.edit.transfer.tsx @@ -144,7 +144,7 @@ export default function EditDeviceTransfer() { const { deviceName, existingTransfer } = useLoaderData() const actionData = useActionData() const navigation = useNavigation() - const { t } = useTranslation('device-transfer') + const { t, i18n } = useTranslation('device-transfer') const [copied, setCopied] = useState(false) @@ -293,7 +293,7 @@ export default function EditDeviceTransfer() {

{t('valid_until')}{' '} - {new Date(transferExpiresAt).toLocaleString(t('locale'), { + {new Date(transferExpiresAt).toLocaleString(i18n.language, { dateStyle: 'medium', timeStyle: 'short', })} diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 64613a36..7a06919d 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -127,7 +127,7 @@ export default function ProfilePage() { deviceSchemas, } = useLoaderData() - const { t } = useTranslation('profile') + const { t, i18n } = useTranslation('profile') const columnsTranslation = useTranslation('data-table') const isOwner = !!profile?.userId && requestingUserId === profile.userId @@ -159,7 +159,7 @@ export default function ProfilePage() {

{t('user_since')}{' '} {new Date(profile?.user?.createdAt || '').toLocaleDateString( - t('locale'), + i18n.language, )}

From 2e026c8195cdb8a83bb00aee3b6bddee1aeb17dd Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Thu, 20 Aug 2026 10:30:41 +0200 Subject: [PATCH 08/17] harden date formatting --- app/lib/date.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/lib/date.ts b/app/lib/date.ts index 89d6e085..f61536f7 100644 --- a/app/lib/date.ts +++ b/app/lib/date.ts @@ -10,18 +10,21 @@ export const dateDiffToNowInWords = (locale: string, date: Date) => { const r = new Intl.RelativeTimeFormat(locale) const now = new Date() const diffInSeconds = Math.round((now.getTime() - date.getTime()) / 1000) + const absDiffInSeconds = Math.abs(diffInSeconds) - if (diffInSeconds < ONE_MINUTE_IN_S) return r.format(-diffInSeconds, 'second') - if (diffInSeconds < ONE_HOUR_IN_S) + if (absDiffInSeconds < ONE_MINUTE_IN_S) + return r.format(-diffInSeconds, 'second') + if (absDiffInSeconds < ONE_HOUR_IN_S) return r.format(-Math.round(diffInSeconds / ONE_MINUTE_IN_S), 'minute') - if (diffInSeconds < ONE_DAY_IN_S) + if (absDiffInSeconds < ONE_DAY_IN_S) return r.format(-Math.round(diffInSeconds / ONE_HOUR_IN_S), 'hour') - if (diffInSeconds < ONE_WEEK_IN_S) + if (absDiffInSeconds < ONE_WEEK_IN_S) return r.format(-Math.round(diffInSeconds / ONE_DAY_IN_S), 'day') - if (diffInSeconds < ONE_MONTH_IN_S) + if (absDiffInSeconds < ONE_MONTH_IN_S) return r.format(-Math.round(diffInSeconds / ONE_WEEK_IN_S), 'week') - if (diffInSeconds < ONE_QUARTER_IN_S) - return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'quarter') - if (diffInSeconds < ONE_YEAR_IN_S) + if (absDiffInSeconds < ONE_QUARTER_IN_S) return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') + if (absDiffInSeconds < ONE_YEAR_IN_S) + return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'quarter') + return r.format(-Math.round(diffInSeconds / ONE_YEAR_IN_S), 'year') } From 228a6d3b319726aeb3a402e25b25e3c78eeafa3b Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 24 Aug 2026 09:52:50 +0200 Subject: [PATCH 09/17] restore original 20 day offset --- app/components/ui/range-picker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/ui/range-picker.tsx b/app/components/ui/range-picker.tsx index d1b9a325..f76f2914 100644 --- a/app/components/ui/range-picker.tsx +++ b/app/components/ui/range-picker.tsx @@ -18,7 +18,7 @@ export function DatePickerWithRange() { const { i18n } = useTranslation() const [date, setDate] = React.useState({ from: new Date(new Date().getFullYear(), 0, 20), - to: new Date(new Date().getFullYear(), 1, 10), + to: new Date(new Date().getFullYear(), 1, 9), }) const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { From f2ff9d098559daea927cc32292f930430cfe8042 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 24 Aug 2026 09:55:48 +0200 Subject: [PATCH 10/17] use more sensible number of days for months, years and quarters intervals --- app/lib/date.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/date.ts b/app/lib/date.ts index f61536f7..f9540ef7 100644 --- a/app/lib/date.ts +++ b/app/lib/date.ts @@ -2,9 +2,9 @@ const ONE_MINUTE_IN_S = 60 const ONE_HOUR_IN_S = 60 * ONE_MINUTE_IN_S const ONE_DAY_IN_S = 24 * ONE_HOUR_IN_S const ONE_WEEK_IN_S = 7 * ONE_DAY_IN_S -const ONE_MONTH_IN_S = 4 * ONE_WEEK_IN_S -const ONE_QUARTER_IN_S = 3 * ONE_MONTH_IN_S -const ONE_YEAR_IN_S = 12 * ONE_MONTH_IN_S +const ONE_MONTH_IN_S = 31 * ONE_DAY_IN_S +const ONE_YEAR_IN_S = 365 * ONE_DAY_IN_S +const ONE_QUARTER_IN_S = ONE_YEAR_IN_S / 4 export const dateDiffToNowInWords = (locale: string, date: Date) => { const r = new Intl.RelativeTimeFormat(locale) From 53d1ae160fa4d5fffe5c784af169bc108eed353c Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 24 Aug 2026 11:34:45 +0200 Subject: [PATCH 11/17] use hydrated state for rendering of timestamps --- app/components/mydevices/dt/columns.tsx | 5 ++++- app/routes/admin.devices._index.tsx | 5 ++++- app/routes/admin.users._index.tsx | 9 +++++++-- app/routes/device.$deviceId.edit.logs.tsx | 9 ++++++--- app/routes/device.$deviceId.edit.transfer.tsx | 14 ++++++++++---- app/routes/profile.$username.tsx | 8 +++++--- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index 3eafa5f5..9bf08baf 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -15,6 +15,7 @@ import { } from '~/components/ui/dropdown-menu' import { type Device } from '~/db/schema' import { DeviceIdCell } from './device-id-cell' +import { useHydrated } from '~/hooks/use-hydrated' export type SenseBox = { id: string @@ -34,6 +35,8 @@ export function getColumns( ): ColumnDef[] { const { t, i18n } = useTranslation const isOwner = opts?.isOwner ?? false + const hydrated = useHydrated() + return [ { accessorKey: 'name', @@ -90,7 +93,7 @@ export function getColumns( }, cell: ({ row }) => { const date = new Date(row.getValue('createdAt')) - return
{date.toLocaleDateString(i18n.language)}
+ return
{hydrated && date.toLocaleDateString(i18n.language)}
}, }, { diff --git a/app/routes/admin.devices._index.tsx b/app/routes/admin.devices._index.tsx index 47ed551b..dafa3ceb 100644 --- a/app/routes/admin.devices._index.tsx +++ b/app/routes/admin.devices._index.tsx @@ -2,6 +2,7 @@ import { Link } from 'react-router' import { type Route } from './+types/admin.devices._index' import { getDevices } from '~/db/models/device.server' import { useTranslation } from 'react-i18next' +import { useHydrated } from '~/hooks/use-hydrated' export async function loader({}: Route.LoaderArgs) { const devices = await getDevices('json') @@ -13,6 +14,7 @@ export default function AdminDevicesIndexRoute({ }: Route.ComponentProps) { const { devices } = loaderData const { i18n } = useTranslation() + const hydrated = useHydrated() return (
@@ -45,7 +47,8 @@ export default function AdminDevicesIndexRoute({ {device.status} - {new Date(device.createdAt).toLocaleString(i18n.language)} + {hydrated && + new Date(device.createdAt).toLocaleString(i18n.language)} @@ -47,10 +50,12 @@ export default function AdminUsersIndexRoute({ {user.role} - {new Date(user.createdAt).toLocaleString(i18n.language)} + {hydrated && + new Date(user.createdAt).toLocaleString(i18n.language)} - {new Date(user.updatedAt).toLocaleString(i18n.language)} + {hydrated && + new Date(user.updatedAt).toLocaleString(i18n.language)} {/* {user.devicesCount} diff --git a/app/routes/device.$deviceId.edit.logs.tsx b/app/routes/device.$deviceId.edit.logs.tsx index 720c76cd..a06629c7 100644 --- a/app/routes/device.$deviceId.edit.logs.tsx +++ b/app/routes/device.$deviceId.edit.logs.tsx @@ -32,6 +32,7 @@ import { } from '~/db/models/log-entry.server' import { type LogEntry } from '~/db/schema/log-entry' import { getUserId } from '~/services/session-service.server' +import { useHydrated } from '~/hooks/use-hydrated' export async function loader({ request, params }: Route.LoaderArgs) { const userId = await getUserId(request) @@ -96,6 +97,7 @@ export default function Logs() { const { toast } = useToast() const { t, i18n } = useTranslation('edit-device-logs') const [newLogContent, setNewLogContent] = useState('') + const hydrated = useHydrated() const submit = useSubmit() @@ -169,9 +171,10 @@ export default function Logs() { {logEntry.content} - {new Date(logEntry.createdAt).toLocaleString( - i18n.language, - )} + {hydrated && + new Date(logEntry.createdAt).toLocaleString( + i18n.language, + )} diff --git a/app/routes/device.$deviceId.edit.transfer.tsx b/app/routes/device.$deviceId.edit.transfer.tsx index 8566397f..06c96b3a 100644 --- a/app/routes/device.$deviceId.edit.transfer.tsx +++ b/app/routes/device.$deviceId.edit.transfer.tsx @@ -17,6 +17,7 @@ import { getDeviceTransfer, createDeviceTransfer, } from '~/services/transfer-service.server' +import { useHydrated } from '~/hooks/use-hydrated' type LoaderData = { deviceId: string @@ -145,6 +146,7 @@ export default function EditDeviceTransfer() { const actionData = useActionData() const navigation = useNavigation() const { t, i18n } = useTranslation('device-transfer') + const hydrated = useHydrated() const [copied, setCopied] = useState(false) @@ -293,10 +295,14 @@ export default function EditDeviceTransfer() {

{t('valid_until')}{' '} - {new Date(transferExpiresAt).toLocaleString(i18n.language, { - dateStyle: 'medium', - timeStyle: 'short', - })} + {hydrated && + new Date(transferExpiresAt).toLocaleString( + i18n.language, + { + dateStyle: 'medium', + timeStyle: 'short', + }, + )}

) : null} diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 7a06919d..86e9b6c6 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -129,6 +129,7 @@ export default function ProfilePage() { const { t, i18n } = useTranslation('profile') const columnsTranslation = useTranslation('data-table') + const hydrated = useHydrated() const isOwner = !!profile?.userId && requestingUserId === profile.userId @@ -158,9 +159,10 @@ export default function ProfilePage() {

{t('user_since')}{' '} - {new Date(profile?.user?.createdAt || '').toLocaleDateString( - i18n.language, - )} + {hydrated && + new Date(profile?.user?.createdAt || '').toLocaleDateString( + i18n.language, + )}

From 5cf66001554ca1f1733b39cf580247383bb4bc6b Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Mon, 24 Aug 2026 11:41:12 +0200 Subject: [PATCH 12/17] fix missing import --- app/routes/profile.$username.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 86e9b6c6..769f23c9 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -18,6 +18,7 @@ import { getInitials } from '~/lib/strings' import { getUserId } from '~/services/session-service.server' import { claimDevice } from '~/services/transfer-service.server' import { userNameFromURl } from '~/services/user-service.server' +import { useHydrated } from '~/hooks/use-hydrated' type ActionData = { success: boolean From 14454e4daf431003bd97637e6fcbd4d08401e019 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 25 Aug 2026 10:29:25 +0200 Subject: [PATCH 13/17] use date and time in mobile overview layer --- app/components/map/layers/mobile/mobile-overview-layer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/map/layers/mobile/mobile-overview-layer.tsx b/app/components/map/layers/mobile/mobile-overview-layer.tsx index ecc2d832..3df4cbd2 100644 --- a/app/components/map/layers/mobile/mobile-overview-layer.tsx +++ b/app/components/map/layers/mobile/mobile-overview-layer.tsx @@ -123,8 +123,8 @@ export default function MobileOverviewLayer({ }) { const { i18n } = useTranslation() const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, { - hour: 'numeric', - minute: '2-digit', + dateStyle: 'short', + timeStyle: 'short', }) // Generate trips and assign colors once From 5ca7c5fa5edae6e9a93bb8a0e3357e8a5242e767 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 25 Aug 2026 10:32:56 +0200 Subject: [PATCH 14/17] remove quarters for textual representation of relative time intervals --- app/lib/date.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/lib/date.ts b/app/lib/date.ts index f9540ef7..56bf0abe 100644 --- a/app/lib/date.ts +++ b/app/lib/date.ts @@ -4,7 +4,6 @@ const ONE_DAY_IN_S = 24 * ONE_HOUR_IN_S const ONE_WEEK_IN_S = 7 * ONE_DAY_IN_S const ONE_MONTH_IN_S = 31 * ONE_DAY_IN_S const ONE_YEAR_IN_S = 365 * ONE_DAY_IN_S -const ONE_QUARTER_IN_S = ONE_YEAR_IN_S / 4 export const dateDiffToNowInWords = (locale: string, date: Date) => { const r = new Intl.RelativeTimeFormat(locale) @@ -22,9 +21,7 @@ export const dateDiffToNowInWords = (locale: string, date: Date) => { return r.format(-Math.round(diffInSeconds / ONE_DAY_IN_S), 'day') if (absDiffInSeconds < ONE_MONTH_IN_S) return r.format(-Math.round(diffInSeconds / ONE_WEEK_IN_S), 'week') - if (absDiffInSeconds < ONE_QUARTER_IN_S) - return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') if (absDiffInSeconds < ONE_YEAR_IN_S) - return r.format(-Math.round(diffInSeconds / ONE_QUARTER_IN_S), 'quarter') + return r.format(-Math.round(diffInSeconds / ONE_MONTH_IN_S), 'month') return r.format(-Math.round(diffInSeconds / ONE_YEAR_IN_S), 'year') } From a5eee716cda5701e317d43db9ceb36a1a906c62c Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 25 Aug 2026 10:36:49 +0200 Subject: [PATCH 15/17] reactivate oxlint rules of hooks --- oxlintrc.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/oxlintrc.json b/oxlintrc.json index c198d5e3..14da6d0c 100644 --- a/oxlintrc.json +++ b/oxlintrc.json @@ -4,9 +4,7 @@ "overrides": [ { "files": ["**/tests/**/*.ts"], - "rules": { - "react-hooks/rules-of-hooks": "off" - } + "rules": {} } ] } From f2236c8586e36f4254f61e14fc352cb39b6ff876 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 25 Aug 2026 10:38:23 +0200 Subject: [PATCH 16/17] nevermind it should stay off for tests --- oxlintrc.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oxlintrc.json b/oxlintrc.json index 14da6d0c..c198d5e3 100644 --- a/oxlintrc.json +++ b/oxlintrc.json @@ -4,7 +4,9 @@ "overrides": [ { "files": ["**/tests/**/*.ts"], - "rules": {} + "rules": { + "react-hooks/rules-of-hooks": "off" + } } ] } From b786a301dc650c951067eeee07f9da7c31b80595 Mon Sep 17 00:00:00 2001 From: David Scheidt Date: Tue, 25 Aug 2026 10:43:49 +0200 Subject: [PATCH 17/17] move hydrated state up one level --- app/components/mydevices/dt/columns.tsx | 3 +-- app/routes/profile.$username.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index 9bf08baf..1262e606 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -15,7 +15,6 @@ import { } from '~/components/ui/dropdown-menu' import { type Device } from '~/db/schema' import { DeviceIdCell } from './device-id-cell' -import { useHydrated } from '~/hooks/use-hydrated' export type SenseBox = { id: string @@ -31,11 +30,11 @@ const colStyle = 'pl-0 dark:text-white' export function getColumns( useTranslation: UseTranslationResponse<'data-table', any>, + hydrated: boolean, opts?: { isOwner?: boolean }, ): ColumnDef[] { const { t, i18n } = useTranslation const isOwner = opts?.isOwner ?? false - const hydrated = useHydrated() return [ { diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 769f23c9..62d2d83d 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -204,7 +204,7 @@ export default function ProfilePage() { {profile?.user?.devices && ( device.archivedAt