Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,914 changes: 7,402 additions & 2,512 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion src/app/api/approvals/__tests__/approvals.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ describe('Approval API route', () => {
const res = await fetch('/api/approvals', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 'approval-2', status: ReviewDecision.REJECTED, reviewedBy: 'u-admin' }),
body: JSON.stringify({
id: 'approval-2',
status: ReviewDecision.REJECTED,
reviewedBy: 'u-admin',
}),
});
const json = await res.json();

Expand Down
12 changes: 7 additions & 5 deletions src/app/api/video-analytics/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ vi.mock('@/../infra/edge-config', () => ({
// ---------------------------------------------------------------------------

function makePost(body: Record<string, unknown>): Promise<Response> {
return POST(new Request('https://example.com/api/video-analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}));
return POST(
new Request('https://example.com/api/video-analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}),
);
}

// ---------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion src/components/notificationcenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Image from 'next/image';
import { BellOff } from 'lucide-react';
import { Notification, NotificationType, useNotifications } from '@/providers/Notificationprovider';
import { EmptyState } from '@/components';
import { getDateTimeFormat } from '@/utils/intlCache';

// ──────────────────────────────────────────────────────────────────────────────
// Icon helpers
Expand All @@ -30,7 +31,7 @@ function timeAgo(date: Date): string {
}

function formatAbsoluteTimestamp(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
return getDateTimeFormat('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
Expand Down
3 changes: 2 additions & 1 deletion src/components/search/SearchResultsVisualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';
import { Eye, User, Clock, Star, ArrowUpRight, ChevronDown } from 'lucide-react';
import { SearchResult } from '../../utils/searchUtils';
import { getDateTimeFormat } from '../../utils/intlCache';

interface SearchResultsVisualizerProps {
results: SearchResult[];
Expand All @@ -16,7 +17,7 @@ export function formatSearchResultDate(value: string | Date): string {
if (Number.isNaN(date.getTime())) {
return '';
}
return new Intl.DateTimeFormat('en-US', {
return getDateTimeFormat('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
Expand Down
8 changes: 3 additions & 5 deletions src/lib/logging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ try {
nodeAsyncLocalStorage = null;
}

export const logContextStorage: AsyncContextStorage<LogContextStore> = nodeAsyncLocalStorage ?? simpleStorage;
export const logContextStorage: AsyncContextStorage<LogContextStore> =
nodeAsyncLocalStorage ?? simpleStorage;

export function runWithLogContext<T>(context: LogContextStore, callback: () => T): T {
return logContextStorage.run(context, callback);
Expand Down Expand Up @@ -238,10 +239,7 @@ class Logger implements AppLogger {
(this.baseContext.correlationId as string) ||
activeRequestId;
const activeTraceId =
payload.traceId ||
contextStore?.traceId ||
(this.baseContext.traceId as string) ||
'';
payload.traceId || contextStore?.traceId || (this.baseContext.traceId as string) || '';

const baseRecord: LogRecord = {
level,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/settings/__tests__/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ describe('useSettingsStore', () => {

expect(useSettingsStore.getState().settings.language).toBe('fr');
});
});
});
10 changes: 5 additions & 5 deletions src/store/cmsStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ describe('cmsStore persist middleware', () => {

it('persists history, historyIndex, and course state to sessionStorage', () => {
const course = { id: '1', title: 'Test Course', description: 'Test', modules: [] };

useCMSStore.getState().setCourse(course);

// Check sessionStorage
const stored = JSON.parse(sessionStorage.getItem('cms-storage') || '{}');
expect(stored.state).toBeDefined();
Expand All @@ -32,7 +32,7 @@ describe('cmsStore persist middleware', () => {
it('rehydrates correctly and allows undo after refresh', async () => {
const course1 = { id: '1', title: 'Course v1', description: '', modules: [] };
const course2 = { id: '1', title: 'Course v2', description: '', modules: [] };

useCMSStore.getState().setCourse(course1);
useCMSStore.getState().updateCourse({ title: 'Course v2' });

Expand Down Expand Up @@ -60,7 +60,7 @@ describe('cmsStore persist middleware', () => {

// Perform undo
useCMSStore.getState().undo();

// Verify undo worked
const stateAfterUndo = useCMSStore.getState();
expect(stateAfterUndo.course.title).toBe('Course v1');
Expand All @@ -75,7 +75,7 @@ describe('cmsStore persist middleware', () => {
const state = useCMSStore.getState();
expect(state.history.length).toBe(20);
expect(state.historyIndex).toBe(19);

// The first 5 should be dropped, so the oldest item is v5
expect(state.history[0].title).toBe('Course v5');
expect(state.history[19].title).toBe('Course v24');
Expand Down
14 changes: 8 additions & 6 deletions src/store/cmsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export const useCMSStore = create<CMSState>()(
set((state) => {
let newHistory = state.history.slice(0, state.historyIndex + 1);
newHistory.push(course);

if (newHistory.length > 20) {
newHistory = newHistory.slice(newHistory.length - 20);
}

return {
course,
history: newHistory,
Expand All @@ -68,7 +68,7 @@ export const useCMSStore = create<CMSState>()(
let newHistory = state.history.slice(0, state.historyIndex + 1);

newHistory.push(updatedCourse);

if (newHistory.length > 20) {
newHistory = newHistory.slice(newHistory.length - 20);
}
Expand Down Expand Up @@ -115,7 +115,9 @@ export const useCMSStore = create<CMSState>()(

updateUploadProgress: (id, progress) => {
set((state) => ({
mediaQueue: state.mediaQueue.map((task) => (task.id === id ? { ...task, progress } : task)),
mediaQueue: state.mediaQueue.map((task) =>
task.id === id ? { ...task, progress } : task,
),
}));
},

Expand All @@ -139,6 +141,6 @@ export const useCMSStore = create<CMSState>()(
history: state.history,
historyIndex: state.historyIndex,
}),
}
)
},
),
);
153 changes: 153 additions & 0 deletions src/utils/__tests__/intlCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
getNumberFormat,
getDateTimeFormat,
clearIntlCache,
getNumberFormatCacheSize,
getDateTimeFormatCacheSize,
} from '../intlCache';

describe('intlCache', () => {
beforeEach(() => {
clearIntlCache();
});

describe('getNumberFormat', () => {
it('returns a valid Intl.NumberFormat instance', () => {
const formatter = getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
expect(formatter).toBeInstanceOf(Intl.NumberFormat);
expect(formatter.format(1234.56)).toBe('$1,234.56');
});

it('caches formatters by locale and options', () => {
const formatter1 = getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
const formatter2 = getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
expect(formatter1).toBe(formatter2);
});

it('creates separate formatters for different locales', () => {
const formatterUS = getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
const formatterDE = getNumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
expect(formatterUS).not.toBe(formatterDE);
expect(formatterUS.format(1234.56)).toBe('$1,234.56');
expect(formatterDE.format(1234.56)).toBe('1.234,56 €');
});

it('creates separate formatters for different options', () => {
const formatter1 = getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
const formatter2 = getNumberFormat('en-US', { style: 'percent' });
expect(formatter1).not.toBe(formatter2);
});

it('handles undefined options', () => {
const formatter = getNumberFormat('en-US');
expect(formatter.format(1234)).toBe('1,234');
});
});

describe('getDateTimeFormat', () => {
it('returns a valid Intl.DateTimeFormat instance', () => {
const formatter = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' });
expect(formatter).toBeInstanceOf(Intl.DateTimeFormat);
});

it('caches formatters by locale and options', () => {
const formatter1 = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' });
const formatter2 = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' });
expect(formatter1).toBe(formatter2);
});
});

describe('cache sizes', () => {
it('returns correct number format cache size', () => {
expect(getNumberFormatCacheSize()).toBe(0);
getNumberFormat('en-US');
getNumberFormat('en-US', { style: 'currency', currency: 'USD' });
expect(getNumberFormatCacheSize()).toBe(2);
});

it('returns correct date time format cache size', () => {
expect(getDateTimeFormatCacheSize()).toBe(0);
getDateTimeFormat('en-US');
expect(getDateTimeFormatCacheSize()).toBe(1);
});

it('clearIntlCache clears both caches', () => {
getNumberFormat('en-US');
getDateTimeFormat('en-US');
expect(getNumberFormatCacheSize()).toBe(1);
expect(getDateTimeFormatCacheSize()).toBe(1);
clearIntlCache();
expect(getNumberFormatCacheSize()).toBe(0);
expect(getDateTimeFormatCacheSize()).toBe(0);
});
});
});

describe('intlCache performance', () => {
beforeEach(() => {
clearIntlCache();
});

it('cached calls are significantly faster than construction', () => {
const iterations = 10000;
const locale = 'en-US';
const options = { style: 'currency' as const, currency: 'USD' };

const startNew = performance.now();
for (let i = 0; i < iterations; i++) {
new Intl.NumberFormat(locale, options).format(1234.56);
}
const timeNew = performance.now() - startNew;

const formatter = getNumberFormat(locale, options);
const startCached = performance.now();
for (let i = 0; i < iterations; i++) {
formatter.format(1234.56);
}
const timeCached = performance.now() - startCached;

expect(timeCached).toBeLessThan(timeNew / 10);
expect(timeNew).toBeGreaterThan(timeCached);
});

it('cache hit is significantly faster than cache miss (construction)', () => {
const iterations = 10000;
const locale = 'en-US';

const startMiss = performance.now();
for (let i = 0; i < iterations; i++) {
getNumberFormat(locale, { maximumFractionDigits: 2 });
}
const timeMiss = performance.now() - startMiss;

const startHit = performance.now();
for (let i = 0; i < iterations; i++) {
getNumberFormat(locale, { maximumFractionDigits: 2 });
}
const timeHit = performance.now() - startHit;

expect(timeHit).toBeLessThan(timeMiss / 10);
});

it('DateFormatter cache provides performance benefit', () => {
const iterations = 10000;
const locale = 'en-US';
const options = { year: 'numeric' as const, month: 'long' as const };

const startNew = performance.now();
for (let i = 0; i < iterations; i++) {
new Intl.DateTimeFormat(locale, options).format(new Date());
}
const timeNew = performance.now() - startNew;

const formatter = getDateTimeFormat(locale, options);
const startCached = performance.now();
for (let i = 0; i < iterations; i++) {
formatter.format(new Date());
}
const timeCached = performance.now() - startCached;

expect(timeCached).toBeLessThan(timeNew / 10);
});
});
3 changes: 2 additions & 1 deletion src/utils/chartUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
generateDateLabels,
generateSampleData,
} from '@/utils/visualizationUtils';
import { getNumberFormat } from './intlCache';

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -108,7 +109,7 @@ export const formatDashboardMetric = (
): string => {
switch (type) {
case 'currency':
return new Intl.NumberFormat('en-US', {
return getNumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
Expand Down
8 changes: 4 additions & 4 deletions src/utils/dateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
* Pass an explicit locale to override; omit it to use the browser/system locale.
*/

import { getDateTimeFormat } from './intlCache';

export function formatDate(
date: Date | string | number,
locale?: string,
options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' },
): string {
return new Intl.DateTimeFormat(locale, options).format(new Date(date));
return getDateTimeFormat(locale, options).format(new Date(date));
}

export function formatShortDate(date: Date | string | number, locale?: string): string {
return formatDate(date, locale, { year: 'numeric', month: 'short', day: 'numeric' });
}

export function formatTime(date: Date | string | number, locale?: string): string {
return new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(
new Date(date),
);
return getDateTimeFormat(locale, { hour: '2-digit', minute: '2-digit' }).format(new Date(date));
}

export function formatRelative(date: Date | string | number, locale?: string): string {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/editorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Editor } from '@tiptap/react';
import { getDateTimeFormat } from './intlCache';

export interface EditorTemplate {
id: string;
Expand Down Expand Up @@ -42,7 +43,7 @@ export const insertTemplate = (editor: Editor, templateId: string) => {
};

export const formatTime = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
return getDateTimeFormat('en-US', {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
Expand Down
Loading
Loading