From 259486c3c8987270bcb5409df47a67beedbc995a Mon Sep 17 00:00:00 2001 From: radiolabme Date: Tue, 14 Apr 2026 04:08:14 -0700 Subject: [PATCH 1/6] feat(utils): add stripMarkdown utility --- src/utils/text.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++++ src/utils/text.ts | 26 ++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/utils/text.test.ts create mode 100644 src/utils/text.ts diff --git a/src/utils/text.test.ts b/src/utils/text.test.ts new file mode 100644 index 0000000..87c06d7 --- /dev/null +++ b/src/utils/text.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest'; +import { stripMarkdown } from './text'; + +describe('stripMarkdown', () => { + it('removes bold markers', () => { + expect(stripMarkdown('Hello **world**')).toBe('Hello world'); + }); + + it('removes bold with underscores', () => { + expect(stripMarkdown('Hello __world__')).toBe('Hello world'); + }); + + it('removes italic markers', () => { + expect(stripMarkdown('Hello *world*')).toBe('Hello world'); + }); + + it('removes heading markers', () => { + expect(stripMarkdown('## Heading')).toBe('Heading'); + expect(stripMarkdown('### Sub heading')).toBe('Sub heading'); + expect(stripMarkdown('# H1')).toBe('H1'); + }); + + it('converts links to link text only', () => { + expect(stripMarkdown('[groundwave.io](https://groundwave.io)')).toBe('groundwave.io'); + }); + + it('removes image syntax entirely', () => { + expect(stripMarkdown('![alt text](https://example.com/img.png)')).toBe(''); + }); + + it('preserves list dashes', () => { + expect(stripMarkdown('- item one')).toBe('- item one'); + }); + + it('preserves horizontal rules', () => { + expect(stripMarkdown('---')).toBe('---'); + }); + + it('handles mixed formatting in one string', () => { + expect(stripMarkdown('**6:00 PM** - [Teams](https://teams.microsoft.com)')).toBe( + '6:00 PM - Teams' + ); + }); + + it('collapses 3+ consecutive newlines to 2', () => { + expect(stripMarkdown('line1\n\n\n\nline2')).toBe('line1\n\nline2'); + }); + + it('trims the result', () => { + expect(stripMarkdown(' \n## Hello\n ')).toBe('Hello'); + }); + + it('handles empty string', () => { + expect(stripMarkdown('')).toBe(''); + }); +}); diff --git a/src/utils/text.ts b/src/utils/text.ts new file mode 100644 index 0000000..704e022 --- /dev/null +++ b/src/utils/text.ts @@ -0,0 +1,26 @@ +/** + * Strip Markdown formatting from a string, producing clean plain text. + * Intended for generating email-friendly plain text from Markdown content. + */ +export function stripMarkdown(raw: string): string { + return ( + raw + // Remove images before links (avoid matching image alt as link text) + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '') + // Convert links: [text](url) → text + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + // Remove heading markers (## Heading → Heading) + .replace(/^#{1,6}\s+/gm, '') + // Remove bold (**text** or __text__) + .replace(/\*\*(.*?)\*\*/gs, '$1') + .replace(/__(.*?)__/gs, '$1') + // Remove italic (*text* or _text_), but not list dashes + .replace(/(? Date: Tue, 14 Apr 2026 04:09:18 -0700 Subject: [PATCH 2/6] feat(events): add EventInviteText component --- src/components/event/EventInviteText.astro | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/components/event/EventInviteText.astro diff --git a/src/components/event/EventInviteText.astro b/src/components/event/EventInviteText.astro new file mode 100644 index 0000000..87e7e39 --- /dev/null +++ b/src/components/event/EventInviteText.astro @@ -0,0 +1,162 @@ +--- +/** + * EventInviteText + * + * Renders a collapsible plain-text meeting invite suitable for + * copy-pasting into an email client. Generated at build time. + */ +import type { CollectionEntry } from 'astro:content'; +import { stripMarkdown } from '../../utils/text'; +import { siteConfig, dateTimeFormats, getVenueTimezone } from '../../site.config'; + +interface Props { + event: CollectionEntry<'events'>; + slug: string; + venueConfig?: { name: string; address: string }; + virtualUrl?: string; + hasInPerson: boolean; + hasVirtual: boolean; +} + +const { event, slug, venueConfig, virtualUrl, hasInPerson, hasVirtual } = Astro.props; +const { title, eventDate, startTime, endTime, eventLink, venue, timezone } = event.data; + +// Resolve timezone: explicit → venue → site default +const tz = timezone ?? getVenueTimezone(venue); + +// Format date as "Tuesday, April 21, 2026" +const dateStr = new Intl.DateTimeFormat(siteConfig.locale, { + ...dateTimeFormats.dateLong, + timeZone: tz, +}).format(eventDate); + +// Timezone abbreviation +const tzAbbr = + new Intl.DateTimeFormat(siteConfig.locale, { + timeZone: tz, + timeZoneName: 'short', + }) + .formatToParts(eventDate) + .find((p) => p.type === 'timeZoneName')?.value ?? 'PT'; + +const timeStr = + startTime && endTime + ? `${startTime} – ${endTime} ${tzAbbr}` + : startTime + ? `${startTime} ${tzAbbr}` + : ''; + +// Build location lines +const locationLines: string[] = []; +if (hasInPerson && venueConfig) { + locationLines.push(`In person: ${venueConfig.name}, ${venueConfig.address}`); +} +if (hasVirtual && virtualUrl) { + locationLines.push(`Online: ${virtualUrl}`); +} + +// Strip markdown from body +const body = event.body ? stripMarkdown(event.body) : ''; + +// Assemble invite text +const pageUrl = `https://microhams.com/events/${slug}`; +const sections: string[] = [title, '', timeStr ? `${dateStr} | ${timeStr}` : dateStr]; + +if (locationLines.length > 0) { + sections.push('', ...locationLines); +} + +if (body) { + sections.push('', '---', '', body, '', '---'); +} + +sections.push('', eventLink ? `More info: ${eventLink}` : `Full details: ${pageUrl}`); + +const inviteText = sections + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); +--- + +
+ Copy invite text +
+

+ Starting point for a meeting reminder email. Copy, paste, and refine as needed. +

+
{inviteText}
+ +
+
+ + + + From efdc15d398c45608abd38a4e20174d0d48ba3bc9 Mon Sep 17 00:00:00 2001 From: radiolabme Date: Tue, 14 Apr 2026 04:11:09 -0700 Subject: [PATCH 3/6] feat(events): add invite text to event pages --- src/pages/events/[slug].astro | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pages/events/[slug].astro b/src/pages/events/[slug].astro index f7302d2..0aec14c 100644 --- a/src/pages/events/[slug].astro +++ b/src/pages/events/[slug].astro @@ -9,6 +9,7 @@ import { getCollection, getEntry } from 'astro:content'; import type { GetStaticPaths } from 'astro'; import PageLayout from '../../layouts/PageLayout.astro'; import EventLogistics from '../../components/event/EventLogistics.astro'; +import EventInviteText from '../../components/event/EventInviteText.astro'; import EventDateTime from '../../components/event/EventDateTime.astro'; import { publishedFilter } from '../../utils/content'; import { formatEventDate } from '../../utils/event-time'; @@ -141,6 +142,16 @@ const virtualUrl = onlineMeetingConfig?.link || event.data.teams?.link || event. /> ) } + + {/* Invite text for copy-paste into email */} + From bb3b903fae5463181bee8c5386702b7d4438010f Mon Sep 17 00:00:00 2001 From: radiolabme Date: Tue, 14 Apr 2026 04:39:16 -0700 Subject: [PATCH 4/6] feat(events): clipboard copy icon on event datetime; simplify footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace details/summary invite text block with inline clipboard icon button next to the event datetime; icon swaps to checkmark on copy - Use standard stacked-rectangles copy icon (Lucide Copy) - Fix clipboard fallback via execCommand textarea method - Remove stray venue address from event page footer meta - Remove footer navigation/connect columns; replace with inline copyright + Contact · GitHub, Contact ordered first - GitHub footer link points to repo issues page --- src/components/SiteFooter.astro | 60 ++++---- src/components/event/EventInviteText.astro | 160 ++++++++++++--------- src/pages/events/[slug].astro | 53 +++---- 3 files changed, 146 insertions(+), 127 deletions(-) diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index 5275065..ab97826 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -1,60 +1,48 @@ --- /** * Site Footer - Global Footer - * - * The main site footer with navigation and copyright. - * Extracted as a component for composition in different layouts. */ ---
diff --git a/src/components/event/EventInviteText.astro b/src/components/event/EventInviteText.astro index 87e7e39..8d21ad9 100644 --- a/src/components/event/EventInviteText.astro +++ b/src/components/event/EventInviteText.astro @@ -2,8 +2,9 @@ /** * EventInviteText * - * Renders a collapsible plain-text meeting invite suitable for - * copy-pasting into an email client. Generated at build time. + * Renders a small clipboard icon button that copies a pre-formatted + * plain-text meeting invite to the clipboard. Invite text is generated + * at build time and stored in a data attribute — no visible preview. */ import type { CollectionEntry } from 'astro:content'; import { stripMarkdown } from '../../utils/text'; @@ -78,85 +79,112 @@ const inviteText = sections .trim(); --- -
- Copy invite text -
-

- Starting point for a meeting reminder email. Copy, paste, and refine as needed. -

-
{inviteText}
- -
-
+ diff --git a/src/pages/events/[slug].astro b/src/pages/events/[slug].astro index 0aec14c..b1eb005 100644 --- a/src/pages/events/[slug].astro +++ b/src/pages/events/[slug].astro @@ -42,9 +42,6 @@ const onlineMeetingConfig = event.data.onlineMeeting ? getOnlineMeeting(event.data.onlineMeeting) : undefined; -// Resolve location: frontmatter overrides venue config -const location = event.data.location || venueConfig?.address; - // Determine event type const isExternal = !!event.data.eventLink; const hasInPerson = !!(event.data.venue || event.data.location); @@ -68,16 +65,26 @@ const virtualUrl = onlineMeetingConfig?.link || event.data.teams?.link || event.

{event.data.title}

- - + +
+ + +
@@ -142,16 +149,6 @@ const virtualUrl = onlineMeetingConfig?.link || event.data.teams?.link || event. /> ) } - - {/* Invite text for copy-paste into email */} -
@@ -163,8 +160,6 @@ const virtualUrl = onlineMeetingConfig?.link || event.data.teams?.link || event. class="stack-3 text-sm" style="border-block-start: 1px solid var(--color-border); padding-block-start: var(--space-4); color: var(--color-text-muted);" > - {location &&
{location}
} - { event.data.contactPerson && (
@@ -197,3 +192,11 @@ const virtualUrl = onlineMeetingConfig?.link || event.data.teams?.link || event. + + From fe0d575de83dfe0510e9dc201eb2ec34a7c04327 Mon Sep 17 00:00:00 2001 From: radiolabme Date: Tue, 14 Apr 2026 04:44:28 -0700 Subject: [PATCH 5/6] ci: remove verify step, bump actions/checkout to v6, align node version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove npm run verify from CI: audit is now handled by Dependabot security alerts; outdated packages handled by Dependabot PRs; Node version enforced by setup-node. Running verify was blocking all Dependabot PRs due to an unfixable transitive CVE in astro's bundled vite (dev tool, not runtime). - Bump actions/checkout@v4 → @v6 in ci.yml and deploy.yml (supersedes open Dependabot PR #29) - Align deploy.yml node-version to 22, matching ci.yml --- .github/workflows/ci.yml | 5 +---- .github/workflows/deploy.yml | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b14ae46..6f5ba31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -22,9 +22,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Verify versions - run: npm run verify - - name: Lint run: npm run lint diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 03d64b2..12b8701 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,12 +24,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '22' cache: 'npm' - name: Install dependencies From c9fbb537e36fc8c47851f532b53806857d387648 Mon Sep 17 00:00:00 2001 From: radiolabme Date: Tue, 14 Apr 2026 04:45:47 -0700 Subject: [PATCH 6/6] chore: remove verify step from pre-push hook --- scripts/pre-push.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/pre-push.ts b/scripts/pre-push.ts index 333be45..518f465 100644 --- a/scripts/pre-push.ts +++ b/scripts/pre-push.ts @@ -177,10 +177,6 @@ async function runContentValidation(): Promise { return run('npx tsx scripts/ai-validate.ts'); } -async function runVersionCheck(): Promise { - return run('npm run verify'); -} - // ============================================================================ // Main Execution // ============================================================================ @@ -196,7 +192,6 @@ async function main() { const stages = [ { name: 'Git Status Check', fn: checkGitStatus, skip: false }, - { name: 'Package Versions', fn: runVersionCheck, skip: false }, { name: 'Lint', fn: runLint, skip: false }, { name: 'Type Check', fn: runTypeCheck, skip: false }, { name: 'Content Validation', fn: runContentValidation, skip: false },