fix: storybook font rendering and shared fonts module #1
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user