From 302caf8896cef4193427b3416a0e2a67364f71ca Mon Sep 17 00:00:00 2001 From: Jonathan Hvid Date: Mon, 11 May 2026 15:56:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(component):=20MembershipCard=20=E2=80=94?= =?UTF-8?q?=20dark=20indigo=20identity=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the CouncilMark slot on /pulse. Dark --ink surface, --ink-text foreground, --ink-muted for labels. Top row: 22px cream circle with the N monogram, COUNCIL · NNN (zero-padded to 3) on the right; defensive fallback COUNCIL · MEMBER when member_number is null. First and last name on separate lines in serif italic, split on the last space so compound surnames hang together. MEMBER SINCE / month-year block sits above the focus-tag pill row. Pills render only when focus_tags is non-empty — no placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/MembershipCard.astro | 145 ++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/components/MembershipCard.astro diff --git a/src/components/MembershipCard.astro b/src/components/MembershipCard.astro new file mode 100644 index 0000000..3e018e3 --- /dev/null +++ b/src/components/MembershipCard.astro @@ -0,0 +1,145 @@ +--- +import type { UserPublic } from '../lib/db'; +import { readFocusTags } from '../lib/format'; + +interface Props { + member: UserPublic; +} + +const { member } = Astro.props; + +// Split on the last space — handles compound surnames better than first-space split. +const parts = member.name.trim().split(/\s+/); +const firstName = parts.length === 1 ? parts[0] : parts.slice(0, -1).join(' '); +const lastName = parts.length === 1 ? '' : parts[parts.length - 1]; + +const numberLabel = member.member_number != null + ? `COUNCIL · ${String(member.member_number).padStart(3, '0')}` + : 'COUNCIL · MEMBER'; + +// Member-since: prefer cab_joined_date, fall back to created_at. +const sinceISO = member.cab_joined_date ?? member.created_at; +const since = new Intl.DateTimeFormat('en-GB', { + month: 'long', year: 'numeric', timeZone: 'Europe/Copenhagen', +}).format(new Date(sinceISO.replace(' ', 'T') + (sinceISO.includes('T') ? '' : 'Z'))); + +const tags = readFocusTags(member.focus_tags); +--- +
+
+ + {numberLabel} +
+ +

+ {firstName} + {lastName && {lastName}} +

+ +

+ Member since + {since} +

+ + {tags.length > 0 && ( +
    + {tags.map(t =>
  • {t}
  • )} +
+ )} +
+ +