Skip to content

Phase 07: Build all public pages and calendar integration - #23

Open
CS-5 wants to merge 6 commits into
overhaul/06-homepagefrom
claude/phase-7-implementation-7jspr3
Open

Phase 07: Build all public pages and calendar integration#23
CS-5 wants to merge 6 commits into
overhaul/06-homepagefrom
claude/phase-7-implementation-7jspr3

Conversation

@CS-5

@CS-5 CS-5 commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

Completes Phase 07 by implementing all remaining public pages and integrating a server-side calendar parser. This phase delivers the full public site structure with content-driven pages, a contact form, and a branded calendar agenda built from Google Calendar feeds.

Key Changes

  • Calendar integration: Added functions/ics.ts — a focused iCalendar parser that reads VEVENTs, resolves timezones, and expands recurrence rules from Google Calendar feeds. Parses events server-side in /api/calendar/[name] to work around CORS restrictions and render a branded agenda on /calendar/[name] pages.

  • Public pages: Implemented all remaining routes:

    • /about — organization history with photo timeline sections replacing legacy's fixed sidebar rails
    • /programs — hub page for program offerings
    • /programs/frc/index and /programs/frc/robots — Biohazard team pages with robot history from markdown collection
    • /programs/fll/index — FIRST LEGO League program page
    • /contact — contact form with sidebar information card
    • /donate — sponsorship information with tiered sponsor cards
    • /sponsors — sponsor directory grouped by tier level
    • /get-involved — embedded Google Form with page context
    • /calendar/[name] — branded calendar agendas for SC2 and FRC programs
  • Contact form: Rebuilt ContactForm.astro on primitives with vanilla JavaScript, Turnstile CAPTCHA integration, and browser Constraint Validation API for error handling (replacing react-hook-form and valibot).

  • Sponsor cards: New SponsorCard.astro component displaying sponsor logos, names, tiers, and partnership years with tier-based color tokens.

  • Stat band: New StatBand.astro component for displaying key metrics (numerals in Source Code Pro over captions) — replaces legacy's gradient stats row and bulleted lists.

  • Photo timeline: New PhotoTimeline.astro component rendering team photos as scroll-snap carousel, replacing legacy's hardcoded sidebar image imports.

  • Prose styling: Added prose-copy utility class for long-form content with proper spacing and typography rules for collection-driven pages.

  • Environment configuration: Added Turnstile site key to Astro's environment schema; updated tooling docs.

  • Asset reorganization: Moved images from public/image/ to src/assets/ with program-specific subdirectories for better organization and Astro image optimization.

  • Cleanup: Removed .lycheeignore file and tools/checks/stale-link-ignores.mjs — no longer needed now that all Phase 07 routes exist.

Notable Implementation Details

  • Calendar parser handles DST transitions by stepping on wall-clock time and converting to absolute time afterward, preventing meeting times from shifting when clocks change.
  • Form validation uses native HTML5 attributes with custom error rendering in the system's design language rather than third-party validation libraries.
  • Sponsor grouping respects tier hierarchy from SPONSOR_LEVELS declaration order; former sponsors get name-only acknowledgment.
  • Photo timeline uses scroll-snap for dense, repeating images without parallax (per DESIGN.md §6).
  • All new pages include proper breadcrumb structured data and theme support where applicable.

https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7

claude added 6 commits August 31, 2026 22:06
Group A of Phase 07. Copy is legacy's, verbatim; the structure is not.

`/about` becomes the single flowing column DESIGN.md §5 calls for, with the two
fixed photo rails retired in favour of two timeline groupings driven by the
`frcTeamPhotos`/`fllTeamPhotos` collections — replacing fourteen hardcoded
imports across two sidebar components. The bulleted count list becomes a stat
band in the data voice (§8).

`/sponsors` groups the collection by level, strongest first, with the tier as a
spec chip rather than legacy's per-level hex applied as an inline background.
Former sponsors, which the legacy page dropped, get a name-only acknowledgment
from the same records.

Two additions along the way, both under DESIGN.md §11's process:

- §8 gains a long-form prose entry (lists, blockquotes, paragraph rhythm), which
  the doc was silent on and the about page cannot be built without. It lands as
  `@utility prose-copy`.
- `StatBand` and `PhotoTimeline` join `ui/`; the program pages in Group C are
  `StatBand`'s second consumer.

Images the two pages need move out of `public/image/` into `src/assets/` so they
go through astro:assets, and the asset optimizer re-encodes the camera masters
among them (3 MB saved).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Group B of Phase 07. Copy is legacy's, verbatim, apart from the hero subheads
the three pages never had.

`ContactForm` replaces react-hook-form, valibot, and `@marsidev/react-turnstile`
with one vanilla script (D3). Validation is the browser's own Constraint
Validation API — the fields carry native attributes, and the script only borrows
the messages so they render in the system's error style instead of a native
bubble. Turnstile renders explicitly into its container once its API arrives,
and the API is injected rather than written into the markup, so a no-JS visitor
is spared the request and the `<noscript>` note serves them instead.

`PUBLIC_TURNSTILE_SITE_KEY` is declared in `astro.config.ts`'s `env.schema`
rather than read from an untyped `import.meta.env`, defaulting to Cloudflare's
always-passes test key so a fresh clone and every preview build a working form.

`/donate` keeps legacy's five methods and the second form behind them, with the
`#questions` anchor and the `Sponsor Question` label the Slack messages sort on.
The one deliberate visual change: legacy gave each card its own accent — green,
blue, orange, red, yellow, and two differently coloured buttons — where §2 allows
one action accent per view, so order and content carry the ranking instead.

`/get-involved` keeps the Google Form, now lazy with its height reserved, behind
a page that says what the form is and offers a way out of the embed.

`TitleBlock` gains the stacked orientation §2.9 already sanctioned for a contact
page's info card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Group C of Phase 07.

`/programs` is new — the hub behind the header's Programs disclosure (D26), and
therefore the whole path on touch and with JavaScript off. Its copy is new too.

`/programs/frc` ports legacy's copy verbatim, with one correction ("engineers
scientists" → "engineers, scientists"). The stats row and the four-item overview
land on `StatBand` and `FeatureCard`; the two `react-scroll-parallax` banners
become one framed media link, since §6 has no parallax in it and the banners
were a link's only content. The hero is the poster still this phase — plan/09
re-encodes the 22 MB video, and `Hero`'s media slot takes a `<video>` unchanged
when it does.

`/programs/fll` replaces an "under construction" placeholder. It has no copy to
port, so what is written is limited to what the site can stand behind: the
program description already running on the homepage, the three parts of a season
as FIRST defines them, and the teams the content collection knows about.
Anything season-specific routes to the calendar and the form.

`/programs/frc/robots` renders the `frcRobots` collection newest-first —
legacy's six hand-written carousel slides, each with its own `<h1>`, are six
markdown files. The carousel itself is gone: a robot history is a list, and one
that hides five of six entries behind arrows is worse for readers and for search.

Closes the `ProgramLayout` seam in `knip.jsonc`: the program pages are its real
consumers, so the entry goes with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Group D of Phase 07, and the end of the phase.

`functions/api/calendar/[name].ts` fetches the public Google Calendar feed
server-side and hands the page JSON. Server-side is not a preference here: the
feed sends no CORS headers, so a browser cannot read it at all. Responses carry
`Cache-Control: public, max-age=900` and go through Cloudflare's edge cache, so
one fetch of the feed serves every visitor for the freshness window.

`functions/ics.ts` is the parser, hand-rolled per plan/07 Group D. It is more
than the minimum that plan allowed for, because recurrence is not optional on
this data: a robotics calendar is mostly repeating build sessions whose single
DTSTART sits months in the past, and a parser that skipped RRULE would render an
empty agenda on a calendar that is anything but. It handles line unfolding,
TZID resolution through the runtime's own zone database, DAILY/WEEKLY/MONTHLY/
YEARLY rules with INTERVAL, COUNT, UNTIL, BYDAY and BYMONTHDAY, EXDATE, and
RECURRENCE-ID overrides. Expansion steps the wall clock rather than absolute
time, so a 6:30pm meeting stays at 6:30pm the week the clocks change.

`/calendar/frc` and `/calendar/sc2` replace Google's embedded iframe with an
agenda in the site's own type and pockets, grouped by day, rendered from that
JSON by a small vanilla script. Feed text reaches the DOM only through
`textContent`. The FRC calendar wears the program theme. The link out to Google
is in the static HTML on every render — not a no-JS afterthought, but because
"add this to my own calendar" is a thing people come here to do.

`.lycheeignore` is gone, as Phase 07's acceptance criteria require, and with it
`tools/checks/stale-link-ignores.mjs` and its CI step: the guard existed only to
police that file's entries, and a check that can now only ever no-op is the same
dead seam the knip entries were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Driven against the production preview in Chromium at 390px and 1440px, with
axe-core over every route (overview standing rule 9; the chrome-devtools MCP
this environment ships could not start, so Playwright drove the same checks).
Every page came back clean on headings, alt text, image dimensions, and
horizontal overflow. Four things did not:

- `Section`'s ghost numeral is taller than a header that carries only a title,
  so on `/sponsors`, `/donate`, `/programs`, and one section of `/about` it
  spilled past the header and sat behind the content's first line — where §2.6
  never puts it. The header now reserves the device's height.
- The calendar agenda was a centred column under a left-aligned header. It now
  aligns with the page, and each row reads time-then-event in two columns once
  there is room for them.
- A robot with no photo yet was pairing its heading with an empty half-page
  frame. A placeholder season now stays one narrow column, and the year moves
  out of the figure caption and above the name, where every entry gets one.
- `/404` skipped from `h1` to `h3` (Phase 05). Its card titles are `h2`.

axe-core now reports no violations on any route at either viewport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Marks the phase file's criteria, and says plainly which two are not met yet:
the contact form's success path and the calendar against Google's live feed
both need a preview deploy, because `astro preview` does not run Pages Functions
and this environment's egress proxy blocks both upstreams. Everything checkable
without them is checked, and how it was checked is written down.

Also moves the two spacing values this phase introduced onto the 4px scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF4NjdDPp9uujjjtKyZuH7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants