diff --git a/src/shared/lib/formatDate.test.ts b/src/shared/lib/formatDate.test.ts new file mode 100644 index 0000000..02ff72e --- /dev/null +++ b/src/shared/lib/formatDate.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { formatYearRange } 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'); + }); + + 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'); + }); + }); + + describe('Error & Edge 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'); + }); + + 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'); + }); + + 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('handles empty strings by throwing', () => { + expect(() => formatYearRange('', null)).toThrow('Invalid start date'); + }); + }); +}); diff --git a/src/shared/lib/formatDate.ts b/src/shared/lib/formatDate.ts new file mode 100644 index 0000000..4fc97df --- /dev/null +++ b/src/shared/lib/formatDate.ts @@ -0,0 +1,31 @@ +/** + * Formats a PocketBase date string into a localized year string 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 { + 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`; + } + + 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 (startYear === endYear) { + return `${startYear}`; + } + + return `${startYear} — ${endYear}`; +} diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index a914abd..e8331fc 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,3 +1,4 @@ -export { cn } from './cn'; export type { ClassValue } from 'clsx'; +export { cn } from './cn'; export * from './fonts'; +export * from './formatDate'; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 2c4ac10..586af4f 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,17 +1,12 @@ -export { Badge } from './Badge'; export type { BadgeVariant } from './Badge'; - +export { Badge } from './Badge'; +export type { ButtonSize, ButtonVariant } from './Button'; export { Button } from './Button'; -export type { ButtonVariant, ButtonSize } from './Button'; - -export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; export type { CardBackground } from './Card'; +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './Card'; export { Input, Textarea } from './Input'; - -export { Section, Container } from './Section'; -export type { SectionBackground, ContainerSize } from './Section'; - -export { SectionAccordion } from './SectionAccordion'; +export type { ContainerSize, SectionBackground } from './Section'; +export { Container, Section } from './Section'; export { TechStackBrick, TechStackGrid } from './TechStack'; diff --git a/src/test/__mocks__/next-font-google.ts b/src/test/__mocks__/next-font-google.ts new file mode 100644 index 0000000..9c99157 --- /dev/null +++ b/src/test/__mocks__/next-font-google.ts @@ -0,0 +1,12 @@ +import { vi } from 'vitest'; + +const mockFont = () => ({ + variable: '--font-mock', + className: 'mock-font', + style: { fontFamily: 'mock-font' }, +}); + +vi.mock('next/font/google', () => ({ + Fraunces: mockFont, + Public_Sans: mockFont, +})); diff --git a/src/test/setup.ts b/src/test/setup.ts index 7b0828b..b166ee9 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1 +1,2 @@ import '@testing-library/jest-dom'; +import './__mocks__/next-font-google';