diff --git a/src/shared/lib/utils/formatDate/formatDate.test.ts b/src/shared/lib/utils/formatDate/formatDate.test.ts index c4e0518..e5dbdfe 100644 --- a/src/shared/lib/utils/formatDate/formatDate.test.ts +++ b/src/shared/lib/utils/formatDate/formatDate.test.ts @@ -1,47 +1,47 @@ -import { formatYearRange } from './formatDate'; +import { formatMonthYearRange } from './formatDate'; -describe('formatYearRange', () => { - describe('Success Paths', () => { - it('formats a date range within the same year', () => { - const start = '2024-01-01 12:00:00.000Z'; - const end = '2024-12-31 12:00:00.000Z'; - expect(formatYearRange(start, end)).toBe('2024'); +describe('formatMonthYearRange', () => { + describe('open-ended range', () => { + it('formats start date with Present when end is null', () => { + expect(formatMonthYearRange('2022-01-01T00:00:00Z', null)).toBe('Jan 2022 — Present'); }); - it('formats a range between different years', () => { - const start = '2021-05-15 12:00:00.000Z'; - const end = '2024-03-20 12:00:00.000Z'; - expect(formatYearRange(start, end)).toBe('2021 — 2024'); - }); - - it('formats a range with null end date as "Present"', () => { - const start = '2022-08-01 12:00:00.000Z'; - const end = null; - expect(formatYearRange(start, end)).toBe('2022 — Present'); + it('uses abbreviated month name', () => { + expect(formatMonthYearRange('2020-08-15T00:00:00Z', null)).toBe('Aug 2020 — Present'); }); }); - describe('Error & Edge Cases', () => { + describe('closed range', () => { + it('formats start and end with month and year', () => { + expect(formatMonthYearRange('2021-05-01T00:00:00Z', '2024-03-31T00:00:00Z')).toBe('May 2021 — Mar 2024'); + }); + + it('handles same year with different months', () => { + expect(formatMonthYearRange('2024-01-01T00:00:00Z', '2024-12-31T00:00:00Z')).toBe('Jan 2024 — Dec 2024'); + }); + + it('handles same month and year', () => { + expect(formatMonthYearRange('2024-06-01T00:00:00Z', '2024-06-30T00:00:00Z')).toBe('Jun 2024'); + }); + }); + + describe('error cases', () => { it('throws if start date is invalid', () => { - const start = 'not-a-date'; - const end = '2024-01-01'; - expect(() => formatYearRange(start, end)).toThrow('Invalid start date'); + expect(() => formatMonthYearRange('not-a-date', null)).toThrow('Invalid start date'); }); it('throws if end date is provided but invalid', () => { - const start = '2024-01-01'; - const end = 'invalid'; - expect(() => formatYearRange(start, end)).toThrow('Invalid end date'); + expect(() => formatMonthYearRange('2024-01-01T00:00:00Z', 'invalid')).toThrow('Invalid end date'); }); - it('throws if start year is after end year', () => { - const start = '2024-01-01'; - const end = '2020-01-01'; - expect(() => formatYearRange(start, end)).toThrow('Start year cannot be after end year'); + it('throws if start is after end', () => { + expect(() => formatMonthYearRange('2024-01-01T00:00:00Z', '2020-01-01T00:00:00Z')).toThrow( + 'Start date cannot be after end date', + ); }); - it('handles empty strings by throwing', () => { - expect(() => formatYearRange('', null)).toThrow('Invalid start date'); + it('throws on empty string', () => { + expect(() => formatMonthYearRange('', null)).toThrow('Invalid start date'); }); }); }); diff --git a/src/shared/lib/utils/formatDate/formatDate.ts b/src/shared/lib/utils/formatDate/formatDate.ts index 4fc97df..a8b353c 100644 --- a/src/shared/lib/utils/formatDate/formatDate.ts +++ b/src/shared/lib/utils/formatDate/formatDate.ts @@ -1,31 +1,38 @@ +const MONTH_FMT = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric', timeZone: 'UTC' }); + +function formatMonthYear(date: Date): string { + return MONTH_FMT.format(date); +} + /** - * Formats a PocketBase date string into a localized year string or "Present". + * Formats a PocketBase date string into a localized month+year range or "Present". * @throws {Error} if any date is invalid or if the range is logically impossible. */ -export function formatYearRange(start: string, end: string | null): string { +export function formatMonthYearRange(start: string, end: string | null): string { const startDate = new Date(start); if (Number.isNaN(startDate.getTime())) { throw new Error('Invalid start date'); } - const startYear = startDate.getFullYear(); if (end === null) { - return `${startYear} — Present`; + return `${formatMonthYear(startDate)} — Present`; } const endDate = new Date(end); if (Number.isNaN(endDate.getTime())) { throw new Error('Invalid end date'); } - const endYear = endDate.getFullYear(); - if (startYear > endYear) { - throw new Error('Start year cannot be after end year'); + if (startDate > endDate) { + throw new Error('Start date cannot be after end date'); } - if (startYear === endYear) { - return `${startYear}`; + const startLabel = formatMonthYear(startDate); + const endLabel = formatMonthYear(endDate); + + if (startLabel === endLabel) { + return startLabel; } - return `${startYear} — ${endYear}`; + return `${startLabel} — ${endLabel}`; } diff --git a/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.test.tsx b/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.test.tsx index 3530baa..8a4c524 100644 --- a/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.test.tsx +++ b/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.test.tsx @@ -65,12 +65,12 @@ describe('ExperienceSection', () => { it('formats open-ended period as "Present"', async () => { render(await ExperienceSection()); - expect(screen.getByText('2022 — Present')).toBeInTheDocument(); + expect(screen.getByText('Jan 2022 — Present')).toBeInTheDocument(); }); - it('formats closed period with year range', async () => { + it('formats closed period with month and year range', async () => { render(await ExperienceSection()); - expect(screen.getByText('2020 — 2021')).toBeInTheDocument(); + expect(screen.getByText('Jan 2020 — Dec 2021')).toBeInTheDocument(); }); it('renders description text', async () => { diff --git a/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.tsx b/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.tsx index 570788b..1293456 100644 --- a/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.tsx +++ b/src/widgets/ExperienceSection/ui/ExperienceSection/ExperienceSection.tsx @@ -1,7 +1,7 @@ import { ExperienceCard } from '$entities/experience'; import type { ExperienceRecord } from '$shared/api'; import { getCollection } from '$shared/api'; -import { formatYearRange } from '$shared/lib'; +import { formatMonthYearRange } from '$shared/lib'; /** * Experience section component. @@ -19,7 +19,7 @@ export default async function ExperienceSection() { key={exp.id} title={exp.role} company={exp.company} - period={formatYearRange(exp.start_date, exp.end_date)} + period={formatMonthYearRange(exp.start_date, exp.end_date)} description={exp.description} stack={exp.stack} />