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:
Hermann_Kitio 2026-04-22 21:29:32 +03:00
parent da4e465125
commit a752029c19
12 changed files with 762 additions and 1 deletions

View 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
View 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')
}