diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts index afaef3339d..d4c70e9102 100644 --- a/frontend/__tests__/e2e/pages/Home.spec.ts +++ b/frontend/__tests__/e2e/pages/Home.spec.ts @@ -71,8 +71,7 @@ test.describe('Home Page', () => { test('should have upcoming events', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Upcoming Events' })).toBeVisible() await expect(page.getByRole('link', { name: 'Event 1' })).toBeVisible() - await expect(page.getByText('Feb 27,')).toBeVisible() - await expect(page.getByText('Feb 28,')).toBeVisible() + await expect(page.getByText('Feb 27 — 28, 2025')).toBeVisible() await page.getByRole('link', { name: 'Event 1' }).click() }) }) diff --git a/frontend/__tests__/unit/pages/Home.test.tsx b/frontend/__tests__/unit/pages/Home.test.tsx index fad8930c17..24aa1d9e42 100644 --- a/frontend/__tests__/unit/pages/Home.test.tsx +++ b/frontend/__tests__/unit/pages/Home.test.tsx @@ -4,7 +4,6 @@ import { mockAlgoliaData, mockGraphQLData } from '@unit/data/mockHomeData' import { fetchAlgoliaData } from 'api/fetchAlgoliaData' import { toast } from 'hooks/useToast' import { Home } from 'pages' -import { formatDate } from 'utils/dateFormatter' import { render } from 'wrappers/testUtil' jest.mock('hooks/useToast', () => ({ @@ -161,9 +160,7 @@ describe('Home', () => { expect(screen.getByText('Upcoming Events')).toBeInTheDocument() mockGraphQLData.upcomingEvents.forEach((event) => { expect(screen.getByText(event.name)).toBeInTheDocument() - expect( - screen.getByText(`${formatDate(event.startDate)} - ${formatDate(event.endDate)}`) - ).toBeInTheDocument() + expect(screen.getByText('Feb 27 — 28, 2025')).toBeInTheDocument() }) }) }) diff --git a/frontend/__tests__/unit/utils/dateFormatter.test.ts b/frontend/__tests__/unit/utils/dateFormatter.test.ts new file mode 100644 index 0000000000..df9470ea74 --- /dev/null +++ b/frontend/__tests__/unit/utils/dateFormatter.test.ts @@ -0,0 +1,84 @@ +import { formatDate, formatDateRange } from 'utils/dateFormatter' + +describe('formatDate function', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('formats ISO date string correctly', () => { + expect(formatDate('2023-09-01')).toBe('Sep 1, 2023') + }) + + test('formats Unix timestamp correctly', () => { + // 1630454400 is Sep 1, 2021 in Unix timestamp (seconds) + expect(formatDate(1630454400)).toBe('Sep 1, 2021') + }) + + test('throws error for invalid date', () => { + expect(() => formatDate('invalid-date')).toThrow('Invalid date') + }) + + test('handles different date formats', () => { + expect(formatDate('2023/09/01')).toBe('Sep 1, 2023') + expect(formatDate('09/01/2023')).toBe('Sep 1, 2023') + }) + + test('formats dates with leading zeros correctly', () => { + expect(formatDate('2023-01-05')).toBe('Jan 5, 2023') + }) + + test('handles different months', () => { + expect(formatDate('2023-12-25')).toBe('Dec 25, 2023') + expect(formatDate('2023-07-04')).toBe('Jul 4, 2023') + }) +}) + +describe('formatDateRange function', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + test('formats date range in same month correctly', () => { + expect(formatDateRange('2023-09-01', '2023-09-04')).toBe('Sep 1 — 4, 2023') + }) + + test('formats date range in different months but same year correctly', () => { + expect(formatDateRange('2023-09-29', '2023-10-02')).toBe('Sep 29 — Oct 2, 2023') + }) + + test('formats date range in different years correctly', () => { + expect(formatDateRange('2023-12-30', '2024-01-03')).toBe('Dec 30, 2023 — Jan 3, 2024') + }) + + test('formats Unix timestamp date ranges correctly', () => { + // Sept 1-4, 2021 + const startTimestamp = 1630454400 // Sep 1, 2021 + const endTimestamp = 1630713600 // Sep 4, 2021 + expect(formatDateRange(startTimestamp, endTimestamp)).toBe('Sep 1 — 4, 2021') + }) + + test('throws error when start date is invalid', () => { + expect(() => formatDateRange('invalid-date', '2023-09-04')).toThrow('Invalid date') + }) + + test('throws error when end date is invalid', () => { + expect(() => formatDateRange('2023-09-01', 'invalid-date')).toThrow('Invalid date') + }) + + test('handles month boundaries correctly', () => { + expect(formatDateRange('2023-09-30', '2023-10-02')).toBe('Sep 30 — Oct 2, 2023') + }) + + test('handles year boundaries correctly', () => { + expect(formatDateRange('2023-12-29', '2024-01-02')).toBe('Dec 29, 2023 — Jan 2, 2024') + }) + + test('handles single-day ranges correctly', () => { + expect(formatDateRange('2023-09-01', '2023-09-01')).toBe('Sep 1, 2023') + }) + + test('handles mixed input types correctly', () => { + // Sep 1, 2021 as Unix timestamp and ISO string + expect(formatDateRange(1630454400, '2021-09-04')).toBe('Sep 1 — 4, 2021') + }) +}) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 41aee67a00..784b08452f 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -18,7 +18,7 @@ import { ChapterTypeAlgolia } from 'types/chapter' import { EventType } from 'types/event' import { MainPageData } from 'types/home' import { capitalize } from 'utils/capitalize' -import { formatDate } from 'utils/dateFormatter' +import { formatDate, formatDateRange } from 'utils/dateFormatter' import AnimatedCounter from 'components/AnimatedCounter' import ChapterMap from 'components/ChapterMap' import ItemCardList from 'components/ItemCardList' @@ -146,11 +146,7 @@ export default function Home() {
- - {event.endDate && event.startDate != event.endDate - ? `${formatDate(event.startDate)} - ${formatDate(event.endDate)}` - : formatDate(event.startDate)} - + {formatDateRange(event.startDate, event.endDate)}
diff --git a/frontend/src/utils/dateFormatter.ts b/frontend/src/utils/dateFormatter.ts index 63f8371a59..77439e57ba 100644 --- a/frontend/src/utils/dateFormatter.ts +++ b/frontend/src/utils/dateFormatter.ts @@ -14,3 +14,44 @@ export const formatDate = (input: number | string) => { day: 'numeric', }) } + +export const formatDateRange = (startDate: number | string, endDate: number | string) => { + const start = typeof startDate === 'number' ? new Date(startDate * 1000) : new Date(startDate) + const end = typeof endDate === 'number' ? new Date(endDate * 1000) : new Date(endDate) + + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + throw new Error('Invalid date') + } + + if ( + start.getTime() === end.getTime() || + (start.getFullYear() === end.getFullYear() && + start.getMonth() === end.getMonth() && + start.getDate() === end.getDate()) + ) { + return formatDate(startDate) + } + + const sameMonth = start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear() + const sameYear = start.getFullYear() === end.getFullYear() + + if (sameMonth) { + // Format as "Month Day - Day, Year" (e.g., "Sep 1 - 4, 2025") + return ( + `${start.toLocaleDateString('en-US', { month: 'short' })} ` + + `${start.getDate()} — ${end.getDate()}, ${start.getFullYear()}` + ) + } else if (sameYear) { + // Different months but same year (e.g., "Sep 29 - Oct 2, 2025") + const startMonth = start.toLocaleDateString('en-US', { month: 'short' }) + const endMonth = end.toLocaleDateString('en-US', { month: 'short' }) + const startDay = start.getDate() + const endDay = end.getDate() + const year = start.getFullYear() + + return `${startMonth} ${startDay} — ${endMonth} ${endDay}, ${year}` + } else { + // Different years (e.g., "Dec 30, 2025 - Jan 3, 2026") + return `${formatDate(startDate)} — ${formatDate(endDate)}` + } +}