feat(historique): page /historique — liste paginée des productions + gating plan (Sprint 3.7)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da4e465125
commit
a752029c19
12 changed files with 762 additions and 1 deletions
43
src/shared/lib/__tests__/date.test.ts
Normal file
43
src/shared/lib/__tests__/date.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { formatRelativeDate } from '../date'
|
||||
|
||||
const NOW = new Date('2026-04-22T12:00:00Z')
|
||||
|
||||
describe('formatRelativeDate', () => {
|
||||
it('il y a quelques secondes', () => {
|
||||
const d = new Date(NOW.getTime() - 10 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toMatch(/seconde/i)
|
||||
})
|
||||
|
||||
it('il y a 5 minutes', () => {
|
||||
const d = new Date(NOW.getTime() - 5 * 60 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toContain('5')
|
||||
expect(formatRelativeDate(d, NOW)).toMatch(/minute/i)
|
||||
})
|
||||
|
||||
it('il y a 3 heures', () => {
|
||||
const d = new Date(NOW.getTime() - 3 * 60 * 60 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toContain('3')
|
||||
expect(formatRelativeDate(d, NOW)).toMatch(/heure/i)
|
||||
})
|
||||
|
||||
it('avant-hier (numeric: auto)', () => {
|
||||
const d = new Date(NOW.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toBe('avant-hier')
|
||||
})
|
||||
|
||||
it('il y a 4 jours', () => {
|
||||
const d = new Date(NOW.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toContain('4')
|
||||
expect(formatRelativeDate(d, NOW)).toMatch(/jour/i)
|
||||
})
|
||||
|
||||
it('la semaine dernière (numeric: auto)', () => {
|
||||
const d = new Date(NOW.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
||||
expect(formatRelativeDate(d, NOW)).toMatch(/semaine/i)
|
||||
})
|
||||
|
||||
it('retourne une chaîne vide sur ISO invalide', () => {
|
||||
expect(formatRelativeDate('not-a-date', NOW)).toBe('')
|
||||
})
|
||||
})
|
||||
32
src/shared/lib/date.ts
Normal file
32
src/shared/lib/date.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Helper de formatage de dates relatives — zéro dépendance (Intl.RelativeTimeFormat).
|
||||
*
|
||||
* Exemple : `formatRelativeDate('2026-04-22T10:00:00Z', now)` → « il y a 2 jours »
|
||||
*
|
||||
* Seuils : secondes → minutes → heures → jours → semaines → mois → années.
|
||||
* Locale fixée à `'fr'` — Expria est monolingue français (cf. DESIGN_SYSTEM.md §10).
|
||||
*/
|
||||
|
||||
const RTF = new Intl.RelativeTimeFormat('fr', { numeric: 'auto' })
|
||||
|
||||
const MINUTE = 60
|
||||
const HOUR = 60 * MINUTE
|
||||
const DAY = 24 * HOUR
|
||||
const WEEK = 7 * DAY
|
||||
const MONTH = 30 * DAY
|
||||
const YEAR = 365 * DAY
|
||||
|
||||
export function formatRelativeDate(iso: string, now: Date = new Date()): string {
|
||||
const then = new Date(iso).getTime()
|
||||
if (!Number.isFinite(then)) return ''
|
||||
const diffSec = Math.round((then - now.getTime()) / 1000)
|
||||
const abs = Math.abs(diffSec)
|
||||
|
||||
if (abs < MINUTE) return RTF.format(Math.round(diffSec / 1), 'second')
|
||||
if (abs < HOUR) return RTF.format(Math.round(diffSec / MINUTE), 'minute')
|
||||
if (abs < DAY) return RTF.format(Math.round(diffSec / HOUR), 'hour')
|
||||
if (abs < WEEK) return RTF.format(Math.round(diffSec / DAY), 'day')
|
||||
if (abs < MONTH) return RTF.format(Math.round(diffSec / WEEK), 'week')
|
||||
if (abs < YEAR) return RTF.format(Math.round(diffSec / MONTH), 'month')
|
||||
return RTF.format(Math.round(diffSec / YEAR), 'year')
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue