Skip to content

Commit a029d8f

Browse files
danbimclaude
andcommitted
refactor: consolidate date formatting with date-fns
Replace three separate inline date formatting implementations with centralized formatDate and formatDualTimestamp utilities powered by date-fns, improving consistency and locale-aware formatting across the job table, status badges, and sites page. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 705df0c commit a029d8f

File tree

13 files changed

+59
-102
lines changed

13 files changed

+59
-102
lines changed

app/components/job-table.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
TableHeader,
1717
TableRow,
1818
} from '~/components/ui/table'
19+
import { formatDate } from '~/lib/format-timestamp'
1920
import type { RankedJobOpening } from '~/services/scoring.service'
2021

2122
type JobTableProps = {
@@ -31,12 +32,6 @@ export function JobTable({
3132
onAppliedClick,
3233
onRowClick,
3334
}: JobTableProps) {
34-
const formatDate = (date: Date | string | null) => {
35-
if (!date) return '-'
36-
const d = typeof date === 'string' ? new Date(date) : date
37-
return d.toLocaleDateString()
38-
}
39-
4035
return (
4136
<Table>
4237
<TableHeader>

app/components/status-badge.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SelectTrigger,
88
} from '~/components/ui/select'
99
import { type ApplicationStatus, applicationStatusEnum } from '~/db/schema'
10+
import { formatDate } from '~/lib/format-timestamp'
1011

1112
type StatusBadgeProps = {
1213
jobId: string
@@ -63,11 +64,6 @@ export function StatusBadge({
6364
const fetcher = useFetcher()
6465
const config = STATUS_CONFIG[status]
6566

66-
const formatDate = (date: Date | null | undefined) => {
67-
if (!date) return ''
68-
return ` ${date.toLocaleDateString()}`
69-
}
70-
7167
const handleStatusChange = (newStatus: string) => {
7268
if (newStatus === 'applied' && onAppliedClick) {
7369
onAppliedClick()
@@ -85,7 +81,7 @@ export function StatusBadge({
8581
<SelectTrigger className="w-auto border-0 p-0 h-auto focus:ring-0 shadow-none">
8682
<Badge variant={config.variant} className={config.className}>
8783
{config.label}
88-
{status === 'applied' && formatDate(appliedAt)}
84+
{status === 'applied' && appliedAt && ` ${formatDate(appliedAt)}`}
8985
</Badge>
9086
</SelectTrigger>
9187
<SelectContent position="popper" align="start">

app/lib/format-timestamp.test.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
import { describe, expect, it, vi } from 'vitest'
2-
import { formatDualTimestamp } from './format-timestamp'
2+
import { formatDate, formatDualTimestamp } from './format-timestamp'
33

44
describe('formatDualTimestamp', () => {
55
it('should format timestamp with relative and absolute parts', () => {
6-
// Mock current time to 2026-01-23 16:03:00
76
vi.useFakeTimers()
87
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
98

109
const timestamp = new Date('2026-01-23T14:03:00')
1110
const result = formatDualTimestamp(timestamp)
1211

13-
expect(result).toBe('2 hours ago (23.01.2026 - 14:03)')
12+
expect(result).toBe('about 2 hours ago (Jan 23, 2026, 2:03 PM)')
1413

1514
vi.useRealTimers()
1615
})
1716

18-
it('should handle "just now" for recent timestamps', () => {
17+
it('should handle recent timestamps', () => {
1918
vi.useFakeTimers()
2019
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
2120

2221
const timestamp = new Date('2026-01-23T16:02:30')
2322
const result = formatDualTimestamp(timestamp)
2423

25-
expect(result).toBe('just now (23.01.2026 - 16:02)')
24+
expect(result).toBe('1 minute ago (Jan 23, 2026, 4:02 PM)')
2625

2726
vi.useRealTimers()
2827
})
@@ -34,7 +33,7 @@ describe('formatDualTimestamp', () => {
3433
const timestamp = new Date('2026-01-20T14:03:00')
3534
const result = formatDualTimestamp(timestamp)
3635

37-
expect(result).toBe('3 days ago (20.01.2026 - 14:03)')
36+
expect(result).toBe('3 days ago (Jan 20, 2026, 2:03 PM)')
3837

3938
vi.useRealTimers()
4039
})
@@ -46,7 +45,7 @@ describe('formatDualTimestamp', () => {
4645
const timestamp = new Date('2026-01-23T15:58:00')
4746
const result = formatDualTimestamp(timestamp)
4847

49-
expect(result).toBe('5 minutes ago (23.01.2026 - 15:58)')
48+
expect(result).toBe('5 minutes ago (Jan 23, 2026, 3:58 PM)')
5049

5150
vi.useRealTimers()
5251
})
@@ -58,55 +57,55 @@ describe('formatDualTimestamp', () => {
5857
const timestamp = new Date('2025-11-23T14:03:00')
5958
const result = formatDualTimestamp(timestamp)
6059

61-
expect(result).toBe('2 months ago (23.11.2025 - 14:03)')
60+
expect(result).toBe('2 months ago (Nov 23, 2025, 2:03 PM)')
6261

6362
vi.useRealTimers()
6463
})
6564

66-
it('should handle singular forms - 1 minute ago', () => {
65+
it('should handle 1 minute ago', () => {
6766
vi.useFakeTimers()
6867
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
6968

7069
const timestamp = new Date('2026-01-23T16:02:00')
7170
const result = formatDualTimestamp(timestamp)
7271

73-
expect(result).toBe('1 minute ago (23.01.2026 - 16:02)')
72+
expect(result).toBe('1 minute ago (Jan 23, 2026, 4:02 PM)')
7473

7574
vi.useRealTimers()
7675
})
7776

78-
it('should handle singular forms - 1 hour ago', () => {
77+
it('should handle about 1 hour ago', () => {
7978
vi.useFakeTimers()
8079
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
8180

8281
const timestamp = new Date('2026-01-23T15:03:00')
8382
const result = formatDualTimestamp(timestamp)
8483

85-
expect(result).toBe('1 hour ago (23.01.2026 - 15:03)')
84+
expect(result).toBe('about 1 hour ago (Jan 23, 2026, 3:03 PM)')
8685

8786
vi.useRealTimers()
8887
})
8988

90-
it('should handle singular forms - 1 day ago', () => {
89+
it('should handle 1 day ago', () => {
9190
vi.useFakeTimers()
9291
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
9392

9493
const timestamp = new Date('2026-01-22T16:03:00')
9594
const result = formatDualTimestamp(timestamp)
9695

97-
expect(result).toBe('1 day ago (22.01.2026 - 16:03)')
96+
expect(result).toBe('1 day ago (Jan 22, 2026, 4:03 PM)')
9897

9998
vi.useRealTimers()
10099
})
101100

102-
it('should handle singular forms - 1 month ago', () => {
101+
it('should handle about 1 month ago', () => {
103102
vi.useFakeTimers()
104103
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
105104

106105
const timestamp = new Date('2025-12-23T16:03:00')
107106
const result = formatDualTimestamp(timestamp)
108107

109-
expect(result).toBe('1 month ago (23.12.2025 - 16:03)')
108+
expect(result).toBe('about 1 month ago (Dec 23, 2025, 4:03 PM)')
110109

111110
vi.useRealTimers()
112111
})
@@ -117,7 +116,7 @@ describe('formatDualTimestamp', () => {
117116

118117
const result = formatDualTimestamp('2026-01-23T14:03:00')
119118

120-
expect(result).toBe('2 hours ago (23.01.2026 - 14:03)')
119+
expect(result).toBe('about 2 hours ago (Jan 23, 2026, 2:03 PM)')
121120

122121
vi.useRealTimers()
123122
})
@@ -128,15 +127,31 @@ describe('formatDualTimestamp', () => {
128127
)
129128
})
130129

131-
it('should handle future dates as "just now"', () => {
130+
it('should handle future dates with "in X" prefix', () => {
132131
vi.useFakeTimers()
133132
vi.setSystemTime(new Date('2026-01-23T16:03:00'))
134133

135134
const timestamp = new Date('2026-01-23T16:08:00')
136135
const result = formatDualTimestamp(timestamp)
137136

138-
expect(result).toBe('just now (23.01.2026 - 16:08)')
137+
expect(result).toBe('in 5 minutes (Jan 23, 2026, 4:08 PM)')
139138

140139
vi.useRealTimers()
141140
})
142141
})
142+
143+
describe('formatDate', () => {
144+
it('should format a Date object', () => {
145+
const result = formatDate(new Date('2026-01-23'))
146+
expect(result).toBe('Jan 23, 2026')
147+
})
148+
149+
it('should format a date string', () => {
150+
const result = formatDate('2026-01-23')
151+
expect(result).toBe('Jan 23, 2026')
152+
})
153+
154+
it('should return dash for null', () => {
155+
expect(formatDate(null)).toBe('-')
156+
})
157+
})

app/lib/format-timestamp.ts

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,17 @@
1+
import { format, formatDistanceToNow } from 'date-fns'
2+
13
export function formatDualTimestamp(date: Date | string): string {
24
const d = typeof date === 'string' ? new Date(date) : date
3-
4-
// Handle invalid dates
55
if (Number.isNaN(d.getTime())) {
66
throw new Error('Invalid date provided')
77
}
8-
9-
const now = new Date()
10-
const diffMs = now.getTime() - d.getTime()
11-
12-
// Handle future dates - treat as "just now"
13-
if (diffMs < 0) {
14-
const day = d.getDate().toString().padStart(2, '0')
15-
const month = (d.getMonth() + 1).toString().padStart(2, '0')
16-
const year = d.getFullYear()
17-
const hours = d.getHours().toString().padStart(2, '0')
18-
const minutes = d.getMinutes().toString().padStart(2, '0')
19-
const absolute = `${day}.${month}.${year} - ${hours}:${minutes}`
20-
return `just now (${absolute})`
21-
}
22-
const diffSeconds = Math.floor(diffMs / 1000)
23-
const diffMinutes = Math.floor(diffSeconds / 60)
24-
const diffHours = Math.floor(diffMinutes / 60)
25-
const diffDays = Math.floor(diffHours / 24)
26-
27-
let relative: string
28-
if (diffMinutes < 1) {
29-
relative = 'just now'
30-
} else if (diffMinutes < 60) {
31-
relative = `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`
32-
} else if (diffHours < 24) {
33-
relative = `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
34-
} else if (diffDays < 30) {
35-
relative = `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
36-
} else {
37-
const diffMonths = Math.floor(diffDays / 30)
38-
relative = `${diffMonths} month${diffMonths === 1 ? '' : 's'} ago`
39-
}
40-
41-
const day = d.getDate().toString().padStart(2, '0')
42-
const month = (d.getMonth() + 1).toString().padStart(2, '0')
43-
const year = d.getFullYear()
44-
const hours = d.getHours().toString().padStart(2, '0')
45-
const minutes = d.getMinutes().toString().padStart(2, '0')
46-
47-
const absolute = `${day}.${month}.${year} - ${hours}:${minutes}`
48-
8+
const relative = formatDistanceToNow(d, { addSuffix: true })
9+
const absolute = format(d, 'PPp')
4910
return `${relative} (${absolute})`
5011
}
12+
13+
export function formatDate(date: Date | string | null): string {
14+
if (!date) return '-'
15+
const d = typeof date === 'string' ? new Date(date) : date
16+
return format(d, 'PP')
17+
}

app/routes/sites.tsx

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TableRow,
2121
} from '~/components/ui/table'
2222
import type { JobPostingSite } from '~/db/schema'
23+
import { formatDualTimestamp } from '~/lib/format-timestamp'
2324
import { jobPostingSiteSchema } from '~/schemas/job-posting-site.schema'
2425
import { jobPostingSiteService } from '~/services/index.server'
2526
import type { Route } from './+types/sites'
@@ -83,31 +84,6 @@ export async function action({ request }: Route.ActionArgs) {
8384
return { success: false }
8485
}
8586

86-
function formatRelative(diffDays: number): string {
87-
if (diffDays <= 0) return 'Today'
88-
if (diffDays === 1) return 'Yesterday'
89-
if (diffDays < 7) return `${diffDays} days ago`
90-
if (diffDays < 30) {
91-
const weeks = Math.floor(diffDays / 7)
92-
return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`
93-
}
94-
const months = Math.floor(diffDays / 30)
95-
return `${months} ${months === 1 ? 'month' : 'months'} ago`
96-
}
97-
98-
function formatLastChecked(date: string | null): string {
99-
if (!date) return 'Never'
100-
const checked = new Date(date)
101-
const diffMs = Date.now() - checked.getTime()
102-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
103-
const relative = formatRelative(diffDays)
104-
const localized = checked.toLocaleString(undefined, {
105-
dateStyle: 'medium',
106-
timeStyle: 'short',
107-
})
108-
return `${relative} (${localized})`
109-
}
110-
11187
export default function Sites() {
11288
const { sites } = useLoaderData<typeof loader>()
11389
const [editSite, setEditSite] = useState<JobPostingSite | null>(null)
@@ -175,7 +151,9 @@ export default function Sites() {
175151
</a>
176152
</TableCell>
177153
<TableCell className="text-muted-foreground">
178-
{formatLastChecked(site.lastCheckedAt as string | null)}
154+
{site.lastCheckedAt
155+
? formatDualTimestamp(site.lastCheckedAt)
156+
: 'Never'}
179157
</TableCell>
180158
<TableCell>
181159
<div className="flex gap-2">

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/screenshots/filters.png

19 Bytes
Loading

docs/screenshots/job-list.png

432 Bytes
Loading

docs/screenshots/notes-panel.png

1.32 KB
Loading

docs/screenshots/sites.png

-460 Bytes
Loading

0 commit comments

Comments
 (0)