diff --git a/src/lib/format.ts b/src/lib/format.ts index 7eb1b91..0fce83e 100644 Binary files a/src/lib/format.ts and b/src/lib/format.ts differ diff --git a/src/pages/pulse.astro b/src/pages/pulse.astro index 722c8bd..b151e53 100644 --- a/src/pages/pulse.astro +++ b/src/pages/pulse.astro @@ -9,7 +9,7 @@ import { getAllCabMembers, getPulseById, getUserVote, castVote, } from '../lib/db'; import { - timeOfDay, tenureSince, relativeTime, + timeOfDay, pulseDateLabel, daysSince, tenureMilestone, relativeTime, eventKindLabel, dispatchSlug, dispatchKindLabel, dispatchKindPigment, dispatchLongPreview, } from '../lib/format'; @@ -52,12 +52,17 @@ if (Astro.request.method === 'POST') { // ── Greeting ─────────────────────────────────────────────────────── const firstName = user.name.split(' ')[0]; -const greeting = `Good ${timeOfDay()}, ${firstName}.`; +const greetingPrefix = `Good ${timeOfDay()}, `; // "Good morning, " +const dateLabel = pulseDateLabel(); // "MONDAY, 11 MAY" const tenureAnchor = user.role === 'cab' && user.cab_joined_date ? user.cab_joined_date : user.created_at; -const tenure = tenureSince(tenureAnchor); +const tenureCopy = tenureMilestone(daysSince(tenureAnchor)); + +const memberNumberLabel = user.member_number != null + ? `MEMBER · ${String(user.member_number).padStart(3, '0')}` + : null; // ── Events ───────────────────────────────────────────────────────── const upcoming = getUpcomingEvents(20); @@ -124,10 +129,17 @@ const members = getAllCabMembers();
-

{greeting}

-

- You've been a member for {tenure}. The team is reading every note you leave. -

+
+

{dateLabel}

+

{greetingPrefix}{firstName}.

+

{tenureCopy}

+
+ {memberNumberLabel && ( +
+

{memberNumberLabel}

+

Founding circle

+
+ )}
@@ -356,21 +368,68 @@ const members = getAllCabMembers(); .cascade { opacity: 1; transform: none; animation: none; } } - /* ── Greeting ─────────────────────────────────────────────────── */ - .greeting { display: flex; flex-direction: column; gap: var(--space-3); } + /* ── Greeting (two-column) ────────────────────────────────────── */ + .greeting { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-6); + align-items: end; + } + .greeting-left { + display: flex; + flex-direction: column; + gap: 10px; + max-width: 30rem; + } + .greeting-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 6px; + } + .greeting-date { + font-family: var(--font-sans); + font-size: 11px; + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--on-surface-variant); + margin: 0; + } .greeting-line { font-family: var(--font-serif); font-weight: 400; - font-size: var(--text-display-md); + font-size: 36px; + line-height: 1.05; letter-spacing: var(--tracking-tight); - line-height: var(--leading-tight); color: var(--on-surface); margin: 0; } - .greeting-sub { + .greeting-first { font-style: italic; } + .greeting-tenure { + font-size: 13px; color: var(--on-surface-variant); - max-width: 48rem; margin: 0; + max-width: 380px; + line-height: var(--leading-relaxed); + } + .greeting-member { + font-family: var(--font-sans); + font-size: 11px; + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--on-surface-variant); + margin: 0; + } + .greeting-circle { + font-family: var(--font-serif); + font-size: 14px; + color: var(--on-surface); + margin: 0; + } + + @media (max-width: 720px) { + .greeting { grid-template-columns: 1fr; align-items: start; } + .greeting-right { align-items: flex-start; } } /* ── Events card (--ink) — more air, prominent hero ───────────── */ diff --git a/tests/tenure-milestone.test.ts b/tests/tenure-milestone.test.ts new file mode 100644 index 0000000..332d180 --- /dev/null +++ b/tests/tenure-milestone.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { tenureMilestone } from '../src/lib/format.js'; + +describe('tenureMilestone — copy variants by day count', () => { + it('0 days reads "Day one."', () => { + expect(tenureMilestone(0)).toBe('Day one. The team is reading every note you leave.'); + }); + + it('1 day reads "Day 2." (off-by-one — day 1 is the first 24h after joining)', () => { + expect(tenureMilestone(1)).toBe('Day 2. The team is reading every note you leave.'); + }); + + it('7 days enters the "{n} days in" bucket', () => { + expect(tenureMilestone(7)).toBe('7 days in. The team is reading every note you leave.'); + }); + + it('22 days reads "A few weeks in."', () => { + expect(tenureMilestone(22)).toBe('A few weeks in. The team is reading every note you leave.'); + }); + + it('60 days reads "{n_months} months in." (months = floor(days/30))', () => { + expect(tenureMilestone(60)).toBe('2 months in. The team is reading every note you leave.'); + }); + + it('200 days reads "Almost a year in." (switches to "Still" suffix)', () => { + expect(tenureMilestone(200)).toBe('Almost a year in. Still reading every note you leave.'); + }); + + it('400 days reads "{n_years} year(s) in."', () => { + expect(tenureMilestone(400)).toBe('1 year in. Still reading every note you leave.'); + expect(tenureMilestone(730)).toBe('2 years in. Still reading every note you leave.'); + }); +});