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
18 changes: 0 additions & 18 deletions components/dashboard-card/days-as-engineer.tsx

This file was deleted.

19 changes: 19 additions & 0 deletions components/dashboard-card/time-as-engineer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { render, screen } from '@testing-library/react';

import { TimeAsSoftwareEngineer } from '@/components/dashboard-card/time-as-engineer';

describe('TimeAsSoftwareEngineer', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2026-07-19T12:00:00Z'));
});

afterEach(() => {
jest.useRealTimers();
});

it('should render elapsed time as years, months and days', () => {
render(<TimeAsSoftwareEngineer />);

expect(screen.getByText('7y, 11m, 18d')).toBeInTheDocument();
});
});
27 changes: 27 additions & 0 deletions components/dashboard-card/time-as-engineer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';

import { DashboardCard } from '@/components/dashboard-card/dashboard-card';
import { calendarDurationBetween } from '@/lib/calendar';

const CAREER_START_DATE = new Date('2018-08-01');

export function TimeAsSoftwareEngineer() {
const [formattedDuration, setFormattedDuration] = useState<string>();

// Computed after mount: the page is statically prerendered, so a
// day-precise value baked at build time would mismatch on hydration.
useEffect(() => {
const { years, months, days } = calendarDurationBetween(
CAREER_START_DATE,
new Date(),
);
setFormattedDuration(`${years}y, ${months}m, ${days}d`);
}, []);

return (
<DashboardCard
header="Time as Software Engineer"
metric={formattedDuration}
/>
);
}
59 changes: 59 additions & 0 deletions lib/calendar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { calendarDurationBetween } from '@/lib/calendar';

describe('calendarDurationBetween', () => {
const start = new Date('2018-08-01');

it('should count full years, months and days since start', () => {
expect(
calendarDurationBetween(start, new Date('2026-07-19T12:00:00Z')),
).toEqual({ years: 7, months: 11, days: 18 });
});

it('should roll over to a new year on the anniversary date', () => {
expect(
calendarDurationBetween(start, new Date('2026-08-01T00:00:00Z')),
).toEqual({ years: 8, months: 0, days: 0 });
});

it('should stay at the previous year until the anniversary', () => {
expect(
calendarDurationBetween(start, new Date('2026-07-31T23:59:59Z')),
).toEqual({ years: 7, months: 11, days: 30 });
});

it('should borrow days from the previous month when end day is smaller', () => {
expect(
calendarDurationBetween(
new Date('2018-08-15'),
new Date('2026-07-10T00:00:00Z'),
),
).toEqual({ years: 7, months: 10, days: 25 });
});

it('should compute in UTC regardless of local timezone offset', () => {
// 23:30 in New York (UTC-4) is already July 20 in UTC
expect(
calendarDurationBetween(start, new Date('2026-07-19T23:30:00-04:00')),
).toEqual({ years: 7, months: 11, days: 19 });
});

it('should cap month-end anchors to shorter months', () => {
expect(
calendarDurationBetween(new Date('2020-01-31'), new Date('2020-03-01')),
).toEqual({ years: 0, months: 1, days: 1 });
});

it('should treat a capped february as a full month', () => {
expect(
calendarDurationBetween(new Date('2020-01-31'), new Date('2021-02-28')),
).toEqual({ years: 1, months: 1, days: 0 });
});

it('should return zeros for the same instant', () => {
expect(calendarDurationBetween(start, new Date('2018-08-01'))).toEqual({
years: 0,
months: 0,
days: 0,
});
});
});
48 changes: 48 additions & 0 deletions lib/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export interface CalendarDuration {
years: number;
months: number;
days: number;
}

function daysInUTCMonth(year: number, monthIndex: number): number {
// Day 0 of the next month is the last day of monthIndex
return new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
}

/**
* Calendar duration between two instants, computed in UTC so the
* result does not depend on the viewer's timezone. Month-end anchors
* cap to shorter months (Jan 31 + 1 month = Feb 29), so days are
* never negative — the same convention as date-fns.
*/
export function calendarDurationBetween(
start: Date,
end: Date,
): CalendarDuration {
let years = end.getUTCFullYear() - start.getUTCFullYear();
let months = end.getUTCMonth() - start.getUTCMonth();

const anchorDay = Math.min(
start.getUTCDate(),
daysInUTCMonth(end.getUTCFullYear(), end.getUTCMonth()),
);
let days = end.getUTCDate() - anchorDay;

if (days < 0) {
months -= 1;
const prevMonthDays = daysInUTCMonth(
end.getUTCFullYear(),
end.getUTCMonth() - 1,
);
days =
end.getUTCDate() +
prevMonthDays -
Math.min(start.getUTCDate(), prevMonthDays);
}
if (months < 0) {
years -= 1;
months += 12;
}

return { years, months, days };
}
4 changes: 2 additions & 2 deletions pages/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Head from 'next/head';

import { BlogViewsCard } from '@/components/dashboard-card/blog-views';
import { DaysAsSoftwareEngineer } from '@/components/dashboard-card/days-as-engineer';
import { GithubFollowers } from '@/components/dashboard-card/github-followers';
import { GitHubStars } from '@/components/dashboard-card/github-stars';
import { MonthlyUsers } from '@/components/dashboard-card/monthly-user';
import { TimeAsSoftwareEngineer } from '@/components/dashboard-card/time-as-engineer';

export default function Dashboard() {
return (
Expand Down Expand Up @@ -53,7 +53,7 @@ export default function Dashboard() {
Statistics
</h2>
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 my-2 w-full">
<DaysAsSoftwareEngineer />
<TimeAsSoftwareEngineer />
<BlogViewsCard />
<GithubFollowers />
<GitHubStars />
Expand Down