Skip to content

Refactor/date fns - #1069

Open
scheidtdav wants to merge 9 commits into
devfrom
refactor/date-fns
Open

Refactor/date fns#1069
scheidtdav wants to merge 9 commits into
devfrom
refactor/date-fns

Conversation

@scheidtdav

@scheidtdav scheidtdav commented Aug 19, 2026

Copy link
Copy Markdown
Member

Type of Change

  • Dependency upgrade
  • Bug fix (non-breaking change)
  • Breaking change
    • e.g. a fixed bug or new feature that may break something else
  • New feature
  • Code quality improvements
    • e.g. refactoring, documentation, tests, tooling, ...

Implementation

Checklist

  • I gave this pull request a meaningful title
  • My pull request is targeting the dev branch
  • I have added documentation to my code
  • I have deleted code that I have commented out

Additional Information

@scheidtdav scheidtdav linked an issue Aug 19, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Improvements
    • Dates and times across device, user, profile, log, transfer, map, and range-picker views now follow the active language setting.
    • Sensor timestamps display localized relative time, including accurate handling of future dates.
    • Map popups and date pickers provide consistent localized date and time formatting.

Walkthrough

The PR replaces selected date-fns formatting with Intl.DateTimeFormat, Intl.RelativeTimeFormat, and the active i18n language across shared components and routes.

Changes

Date localization

Layer / File(s) Summary
Relative-time formatter
app/lib/date.ts
Adds dateDiffToNowInWords with threshold-based localized relative-time units.
Component date formatting
app/components/device-detail/device-detail-box.tsx, app/components/map/layers/mobile/mobile-overview-layer.tsx, app/components/ui/range-picker.tsx
Uses the active i18n language for sensor ages, map popup times, and date-range display.
Route date formatting
app/routes/admin.devices._index.tsx, app/routes/admin.users._index.tsx, app/routes/device.$deviceId.edit.logs.tsx, app/routes/device.$deviceId.edit.transfer.tsx, app/routes/profile.$username.tsx, app/components/mydevices/dt/columns.tsx
Passes the active i18n language to creation, update, log, transfer, profile, and device date formatters.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 2e026

The date refactor changes default date ranges and relative-time labels, and may render different timestamps between the server and browser; users could see an incorrect range, misleading elapsed-time text, or hydration errors. Merge should wait until these behaviors are corrected or explicitly accepted by the owner.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes linked issue references but provides no implementation details or selected change type. Select the change type and summarize the date-fns replacement, browser-native formatting, and affected behavior.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes replace date-fns formatting and relative-time usage with browser-native Intl APIs across affected files [#1061].
Out of Scope Changes check ✅ Passed The changed files support the linked objective of replacing date-fns formatting and relative-time functionality with browser-native APIs [#1061].
Title check ✅ Passed The title identifies the main change: refactoring date-fns usage.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/date-fns

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 65.97% 2242 / 3398
🔵 Statements 64.51% 2316 / 3590
🔵 Functions 63.21% 433 / 685
🔵 Branches 50.79% 1080 / 2126
File CoverageNo changed files found.
Generated in workflow #2885 for commit 2e026c8 by the Vitest Coverage Report Action

@scheidtdav scheidtdav linked an issue Aug 20, 2026 that may be closed by this pull request
3 tasks
@scheidtdav
scheidtdav marked this pull request as ready for review August 20, 2026 08:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b4d4e3bc-162f-46da-8926-4951b398f3c8

📥 Commits

Reviewing files that changed from the base of the PR and between 3f79a72 and 2e026c8.

📒 Files selected for processing (11)
  • app/components/device-detail/device-detail-box.tsx
  • app/components/device-detail/graph.tsx
  • app/components/map/layers/mobile/mobile-overview-layer.tsx
  • app/components/mydevices/dt/columns.tsx
  • app/components/ui/range-picker.tsx
  • app/lib/date.ts
  • app/routes/admin.devices._index.tsx
  • app/routes/admin.users._index.tsx
  • app/routes/device.$deviceId.edit.logs.tsx
  • app/routes/device.$deviceId.edit.transfer.tsx
  • app/routes/profile.$username.tsx
💤 Files with no reviewable changes (1)
  • app/components/device-detail/graph.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines 17 to +22
export function DatePickerWithRange() {
const { i18n } = useTranslation()
const [date, setDate] = React.useState<DateRange | undefined>({
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),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- current file ---'
cat -n app/components/ui/range-picker.tsx | sed -n '1,80p'

printf '%s\n' '--- recent file history ---'
git log --oneline -5 -- app/components/ui/range-picker.tsx

printf '%s\n' '--- parent version of the relevant file ---'
parent="$(git rev-parse HEAD^)"
git show "$parent:app/components/ui/range-picker.tsx" | cat -n | sed -n '1,80p'

printf '%s\n' '--- relevant diff ---'
git diff --unified=20 HEAD^ HEAD -- app/components/ui/range-picker.tsx

Repository: openSenseMap/frontend

Length of output: 4674


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- commits that changed addDays usage ---'
git log --all --oneline -S'addDays' -- app/components/ui/range-picker.tsx

printf '%s\n' '--- historical range-picker versions ---'
for commit in 4dd61e5 d493050 4205b56 HEAD; do
	printf '\n[%s]\n' "$commit"
	git show "$commit:app/components/ui/range-picker.tsx" 2>/dev/null \
		| cat -n | sed -n '1,35p' || true
done

printf '%s\n' '--- addDays references in repository ---'
rg -n 'addDays|new Date\\(.*0, 20|new Date\\(.*1, 10' --glob '!node_modules' .

Repository: openSenseMap/frontend

Length of output: 5460


Preserve the original 20-day range offset.

addDays(January 20, 20) returns February 9. Set to to February 9 to preserve the previous default range.

Comment on lines +24 to 28
const dateTimeFormat = new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'long',
day: 'numeric',
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

files=(
  "app/components/ui/range-picker.tsx"
  "app/components/map/layers/mobile/mobile-overview-layer.tsx"
  "app/components/mydevices/dt/columns.tsx"
  "app/routes/admin.devices._index.tsx"
  "app/routes/admin.users._index.tsx"
  "app/routes/device.\$deviceId.edit.logs.tsx"
  "app/routes/device.\$deviceId.edit.transfer.tsx"
  "app/routes/profile.\$username.tsx"
)

for file in "${files[@]}"; do
  echo "===== $file ====="
  wc -l "$file"
  rg -n -C 4 'Intl\.DateTimeFormat|toLocale(Date|String)|dateTimeFormat|useTranslation|createdAt|updatedAt|startTime|endTime|transferExpiresAt' "$file" || true
done

echo "===== SSR/client boundaries and timezone configuration ====="
rg -n -S 'clientOnly|ClientOnly|Hydrat|timeZone|timezone|TZ=|process\.env\.TZ|defaultTimeZone|DateTimeFormat' app package.json vite.config.* remix.config.* i18n* 2>/dev/null || true

echo "===== route/component usage ====="
rg -n -S 'RangePicker|MobileOverviewLayer|admin\.devices|admin\.users|edit\.logs|edit\.transfer|profile\.' app | head -300

Repository: openSenseMap/frontend

Length of output: 18711


🏁 Script executed:

#!/bin/bash
set -eu

echo "===== relevant source context ====="
sed -n '1,75p' app/components/ui/range-picker.tsx
sed -n '105,145p;430,480p' app/components/map/layers/mobile/mobile-overview-layer.tsx
sed -n '1,110p' app/components/mydevices/dt/columns.tsx
sed -n '1,75p' app/routes/admin.devices._index.tsx
sed -n '1,65p' app/routes/admin.users._index.tsx
sed -n '85,110p;160,182p' 'app/routes/device.$deviceId.edit.logs.tsx'
sed -n '135,160p;286,304p' 'app/routes/device.$deviceId.edit.transfer.tsx'
sed -n '120,170p;190,215p' 'app/routes/profile.$username.tsx'

echo "===== framework and hydration setup ====="
sed -n '1,90p' app/entry.server.tsx 2>/dev/null || true
sed -n '1,80p' app/entry.client.tsx
sed -n '1,55p' app/components/client-only.tsx
rg -n -S 'renderTo|hydrate|SSR|ssr|ClientOnly|DatePickerWithRange|MobileOverviewLayer|getColumns|DataTable' app --glob '*.{ts,tsx}' | head -250

echo "===== date/time configuration ====="
rg -n -S 'timeZone|timezone|TZ=|process\.env\.TZ|resolvedLanguage|supportedLngs|fallbackLng|i18n' . --glob '!node_modules' --glob '!build' --glob '!dist' | head -300

echo "===== standalone Intl default-time-zone probe ====="
TZ=UTC node - <<'JS'
const d = new Date('2024-01-15T23:30:00Z')
console.log('UTC:', new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(d))
JS
TZ=America/Los_Angeles node - <<'JS'
const d = new Date('2024-01-15T23:30:00Z')
console.log('Los_Angeles:', new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'short' }).format(d))
JS
TZ=UTC node - <<'JS'
const d = new Date('2024-01-15T23:30:00Z')
console.log('explicit UTC:', new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC'
}).format(d))
JS
TZ=America/Los_Angeles node - <<'JS'
const d = new Date('2024-01-15T23:30:00Z')
console.log('explicit UTC:', new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium', timeStyle: 'short', timeZone: 'UTC'
}).format(d))
JS

Repository: openSenseMap/frontend

Length of output: 42073


🏁 Script executed:

#!/bin/bash
set -eu

echo "===== DataTable render path ====="
sed -n '1,180p' app/components/mydevices/dt/data-table.tsx
sed -n '1,145p' 'app/routes/explore.$deviceId.tsx'

echo "===== range-picker and mobile state/output conditions ====="
rg -n -C 5 'DatePickerWithRange|popupInfo|setPopupInfo|MobileOverviewLayer|ClientOnly|useHydrated' app --glob '*.{ts,tsx}'

echo "===== date-only versus instant formatting probe ====="
node - <<'JS'
const instant = new Date('2024-01-15T23:30:00Z')
const dateOnly = new Date(2024, 0, 20)

for (const tz of ['UTC', 'America/Los_Angeles']) {
  process.env.TZ = tz
  console.log(tz, {
    instantDate: instant.toLocaleDateString('en-US'),
    instantDateTime: instant.toLocaleString('en-US'),
    dateOnlyDate: dateOnly.toLocaleDateString('en-US'),
    dateOnlyDateTime: dateOnly.toLocaleString('en-US'),
  })
}
JS

Repository: openSenseMap/frontend

Length of output: 27119


Use a deterministic time zone for SSR-rendered timestamps.

These formatters omit timeZone. Different server and browser time zones can produce different strings for the same timestamp and cause hydration mismatches. Add an explicit timeZone, or defer formatting until the client.

Apply this to:

  • app/components/mydevices/dt/columns.tsx:93
  • app/routes/admin.devices._index.tsx:48
  • app/routes/admin.users._index.tsx:50-53
  • app/routes/device.$deviceId.edit.logs.tsx:172-174
  • app/routes/device.$deviceId.edit.transfer.tsx:296-299
  • app/routes/profile.$username.tsx:161-163

The range picker uses local date-only values. The mobile popup is populated by client-side map events, so neither confirms this hydration issue.

📍 Affects 8 files
  • app/components/ui/range-picker.tsx#L24-L28 (this comment)
  • app/components/ui/range-picker.tsx#L44-L48
  • app/components/map/layers/mobile/mobile-overview-layer.tsx#L124-L128
  • app/components/map/layers/mobile/mobile-overview-layer.tsx#L460-L470
  • app/components/mydevices/dt/columns.tsx#L35-L35
  • app/components/mydevices/dt/columns.tsx#L93-L93
  • app/routes/admin.devices._index.tsx#L15-L15
  • app/routes/admin.devices._index.tsx#L48-L48
  • app/routes/admin.users._index.tsx#L15-L15
  • app/routes/admin.users._index.tsx#L50-L53
  • app/routes/device.$deviceId.edit.logs.tsx#L97-L97
  • app/routes/device.$deviceId.edit.logs.tsx#L172-L174
  • app/routes/device.$deviceId.edit.transfer.tsx#L147-L147
  • app/routes/device.$deviceId.edit.transfer.tsx#L296-L299
  • app/routes/profile.$username.tsx#L130-L130
  • app/routes/profile.$username.tsx#L162-L163

Source: Linters/SAST tools

Comment thread app/lib/date.ts
Comment on lines +1 to +7
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -t f -i 'date' . | head -80

printf '%s\n' '--- date symbols and references ---'
rg -n --glob '!node_modules' 'ONE_(MONTH|YEAR|QUARTER|WEEK|DAY|HOUR|MINUTE)_IN_S|app/lib/date|format.*date|relative' app tests . 2>/dev/null | head -240

printf '%s\n' '--- date.ts outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline app/lib/date.ts
else
  wc -l app/lib/date.ts
fi

printf '%s\n' '--- app/lib/date.ts ---'
cat -n app/lib/date.ts

Repository: openSenseMap/frontend

Length of output: 30854


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact call sites ---'
rg -n --glob '!node_modules' 'dateDiffToNowInWords' .

printf '%s\n' '--- test/config files ---'
git ls-files | rg '(^|/)(package\.json|.*date.*spec|.*date.*test|vitest|jest|README|CHANGELOG|CONTRIBUTING)' | head -160

printf '%s\n' '--- app/lib/date.ts history summary ---'
git diff --stat -- app/lib/date.ts
git diff -- app/lib/date.ts | head -220

printf '%s\n' '--- behavioral boundary calculation ---'
node - <<'JS'
const MINUTE = 60;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const MONTH = 4 * WEEK;
const QUARTER = 3 * MONTH;
const YEAR = 12 * MONTH;

function bucket(ageDays) {
  const diff = Math.round(ageDays * DAY);
  const abs = Math.abs(diff);
  if (abs < MINUTE) return ['second', -diff];
  if (abs < HOUR) return ['minute', -Math.round(diff / MINUTE)];
  if (abs < DAY) return ['hour', -Math.round(diff / HOUR)];
  if (abs < WEEK) return ['day', -Math.round(diff / DAY)];
  if (abs < MONTH) return ['week', -Math.round(diff / WEEK)];
  if (abs < QUARTER) return ['month', -Math.round(diff / MONTH)];
  if (abs < YEAR) return ['quarter', -Math.round(diff / QUARTER)];
  return ['year', -Math.round(diff / YEAR)];
}
for (const days of [27, 27.99, 28, 29, 56, 83, 84, 335, 336, 350, 365, 366]) {
  console.log(`${days} days -> ${bucket(days)[1]} ${bucket(days)[0]}`);
}
for (const days of [350, 365, 366]) {
  const d = new Date(Date.UTC(2025, 0, 1));
  d.setUTCDate(d.getUTCDate() - days);
  const elapsed = (Date.UTC(2025, 0, 1) - d.getTime()) / 86400000;
  console.log(`calendar probe: ${days} requested -> ${d.toISOString()} -> ${elapsed} elapsed days`);
}
JS

Repository: openSenseMap/frontend

Length of output: 1248


Use calendar-aware differences for month and year output.

ONE_YEAR_IN_S is 336 days, so a 350-day-old timestamp is formatted as 1 year ago. Month and quarter output also uses fixed 28-day blocks. If fixed durations are intentional, document and test this policy.

@scheidtdav

Copy link
Copy Markdown
Member Author

Gotta do the code rabbit reviews still..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

date format mismatch on my devices page Replace date-fns dependency with browser-native implementation

1 participant