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
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,9 +22,6 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Verify versions
run: npm run verify

- name: Lint
run: npm run lint

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 0 additions & 5 deletions scripts/pre-push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,6 @@ async function runContentValidation(): Promise<boolean> {
return run('npx tsx scripts/ai-validate.ts');
}

async function runVersionCheck(): Promise<boolean> {
return run('npm run verify');
}

// ============================================================================
// Main Execution
// ============================================================================
Expand All @@ -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 },
Expand Down
60 changes: 24 additions & 36 deletions src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
@@ -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.
*/
---

<footer class="site-footer">
<div class="wrapper">
<div class="site-footer__inner">
<div class="stack-2">
<div class="stack-1">
<p class="font-semibold">MicroHAMS</p>
<p class="text-sm">Amateur Radio Community</p>
</div>

<div class="stack-2">
<p class="text-sm font-semibold">Navigation</p>
<nav>
<ul class="footer-links" role="list">
<li><a href="/">Home</a></li>
<li><a href="/events">Events</a></li>
<li><a href="/articles">Articles</a></li>
<li><a href="/docs">Docs</a></li>
</ul>
</nav>
<div class="site-footer__meta stack-1 text-xs">
<p>© {new Date().getFullYear()} MicroHAMS</p>
<p>
<a href="/about#location--contact">Contact</a>
<span aria-hidden="true">·</span>
<a href="https://github.com/MicrohamsARC/MicrohamsARC.github.io/issues">GitHub</a>
</p>
</div>

<div class="stack-2">
<p class="text-sm font-semibold">Connect</p>
<ul class="footer-links" role="list">
<li><a href="https://github.com/MicrohamsARC/MicrohamsARC.github.io/">GitHub</a></li>
<li><a href="/about#location--contact">Contact</a></li>
</ul>
</div>
</div>

<div class="text-center" style="margin-block-start: var(--space-8);">
<p class="text-xs">
© {new Date().getFullYear()} MicroHAMS. Built with <a href="https://astro.build">Astro</a>.
</p>
</div>
</div>
</footer>

<style>
.footer-links {
list-style: none;
padding: 0;
margin: 0;
.site-footer__inner {
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-4);
}

.site-footer__meta {
color: var(--color-text-muted);
}

.site-footer__meta a,
.site-footer__meta p {
color: inherit;
}

.footer-links a {
font-size: var(--text-sm);
.site-footer__meta span {
margin-inline: var(--space-1);
}
</style>
190 changes: 190 additions & 0 deletions src/components/event/EventInviteText.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
/**
* EventInviteText
*
* 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';
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();
---

<button
class="invite-copy-btn"
type="button"
title="Copy invite text"
aria-label="Copy invite text to clipboard"
data-invite-text={inviteText}
>
<!-- Copy icon (stacked rectangles) -->
<svg
class="invite-copy-btn__icon invite-copy-btn__icon--clipboard"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="8" y="8" width="12" height="12" rx="2"></rect>
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
</svg>
<!-- Checkmark icon (shown briefly after copy) -->
<svg
class="invite-copy-btn__icon invite-copy-btn__icon--check"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</button>

<script>
document.querySelectorAll<HTMLButtonElement>('.invite-copy-btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const text = btn.dataset.inviteText ?? '';

async function doCopy() {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// fall through
}
}
// execCommand fallback
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
}

const ok = await doCopy();
if (ok) {
btn.classList.add('invite-copy-btn--copied');
setTimeout(() => btn.classList.remove('invite-copy-btn--copied'), 2000);
}
});
});
</script>

<style>
.invite-copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 4px;
border: none;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-sm, 2px);
vertical-align: middle;
transition: color 0.15s;
}

.invite-copy-btn:hover {
color: var(--color-text);
}

.invite-copy-btn__icon--check {
display: none;
color: var(--color-success, currentColor);
}

.invite-copy-btn--copied .invite-copy-btn__icon--clipboard {
display: none;
}

.invite-copy-btn--copied .invite-copy-btn__icon--check {
display: inline;
}
</style>
Loading