diff --git a/frontend/src/lib/thumbnails.test.ts b/frontend/src/lib/thumbnails.test.ts new file mode 100644 index 00000000..aad48268 --- /dev/null +++ b/frontend/src/lib/thumbnails.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { thumbnailUrl } from './thumbnails'; + +describe('thumbnailUrl', () => { + it('returns null when thumbnailKey is undefined', () => { + expect(thumbnailUrl({ id: 'abc' })).toBeNull(); + }); + + it('returns url without version param when thumbnailKey present but generatedAt missing', () => { + expect(thumbnailUrl({ id: 'abc', thumbnailKey: 'thumbnails/abc.jpg' })).toBe( + '/api/documents/abc/thumbnail' + ); + }); + + it('appends encoded cache-bust param when generatedAt present', () => { + const url = thumbnailUrl({ + id: 'abc', + thumbnailKey: 'thumbnails/abc.jpg', + thumbnailGeneratedAt: '2026-04-22T20:41:15.123456' + }); + expect(url).toBe('/api/documents/abc/thumbnail?v=2026-04-22T20%3A41%3A15.123456'); + }); + + it('different generatedAt produces different URL — enables cache-bust on file replace', () => { + const a = thumbnailUrl({ + id: 'x', + thumbnailKey: 'thumbnails/x.jpg', + thumbnailGeneratedAt: '2026-01-01T10:00:00' + }); + const b = thumbnailUrl({ + id: 'x', + thumbnailKey: 'thumbnails/x.jpg', + thumbnailGeneratedAt: '2026-01-01T11:00:00' + }); + expect(a).not.toBe(b); + }); +}); diff --git a/frontend/src/lib/thumbnails.ts b/frontend/src/lib/thumbnails.ts new file mode 100644 index 00000000..47a0a606 --- /dev/null +++ b/frontend/src/lib/thumbnails.ts @@ -0,0 +1,18 @@ +type ThumbnailDoc = { + id: string; + thumbnailKey?: string; + thumbnailGeneratedAt?: string; +}; + +/** + * Builds the URL for a document thumbnail image, or returns null when the document + * has no thumbnail yet. When `thumbnailGeneratedAt` is present it is appended as a + * `?v=…` query param so the browser / proxy cache is invalidated whenever the file + * is replaced (the backend regenerates thumbnails at the same S3 key on replace). + */ +export function thumbnailUrl(doc: ThumbnailDoc): string | null { + if (!doc.thumbnailKey) return null; + const base = `/api/documents/${doc.id}/thumbnail`; + if (!doc.thumbnailGeneratedAt) return base; + return `${base}?v=${encodeURIComponent(doc.thumbnailGeneratedAt)}`; +}