Skip to content

feat: speakers lineup page synced from Sessionize#228

Merged
ryzizub merged 27 commits into
2026from
claude/speakers-session-brainstorm-4dd1d4
Jul 9, 2026
Merged

feat: speakers lineup page synced from Sessionize#228
ryzizub merged 27 commits into
2026from
claude/speakers-session-brainstorm-4dd1d4

Conversation

@ryzizub

@ryzizub ryzizub commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Adds a /speakers lineup page for DevFest.cz 2026, fed by a daily Cloud Function that mirrors the Sessionize All-data view into a public-read Firestore speakers collection. Cards follow the approved /team film-noir treatment (grayscale headshots, cursor colour-bleed hover). Also drops the "2025 Edition" link from the top nav.

Data flow: Sessionize → daily Cloud Function → Firestore speakers → browser island — mirroring the existing tickets pipeline, with Firestore instead of RTDB for structured docs + explicit ordering.

Why

The site had no way to show who's speaking. The org maintains the roster in Sessionize (its source of truth); a daily sync keeps the page current with zero per-speaker code edits.

Behavior

  • refreshSessionize (onSchedule, daily 06:00 Europe/Prague, europe-west1) fetches …/view/All, extracts + validates the speakers[], and writes the full speaker record (bio, sessions, categories, Q&A, isTopSpeaker + sanitized/kind-mapped links + order) in one atomic batch. Sessions/rooms/categories are on the wire but not persisted yet.
  • Guards (a truncated fetch must never wipe the live page): aborts on non-200 / parse error / non-object payload / empty or duplicate-id speakers; withholds deletes when they'd remove >50% of the collection; 15s fetch timeout; best-effort Slack alert that never masks the real error.
  • /speakers pageclient:load React island subscribes live (onSnapshot, orderBy('order')) with loading / ready / empty ("Lineup announced soon") / error states. Per-card fallbacks: missing/broken photo → monogram (onError), missing tagline omitted, long text clamped, unknown link type → globe. <noscript> fallback for no-JS/crawlers (client-only render is an accepted v1 tradeoff, matching Tickets).
  • Security: firestore.rules opens read on speakers only — invoices (PII) and the default stay deny-all; client writes blocked (Admin SDK writes). App Check attach-only (enforcement is a later console toggle). Link URLs sanitized to http(s) + userinfo-stripped at write time, re-checked at the render boundary.
  • Nav: Speakers entry added; 2025 Edition removed.

Files

  • functions/src/sessionize/ — new domain: params.ts (endpoint-id secret), sessionize-api.ts (pure fetch/validate/normalize + delete-guard), refresh-sessionize.ts (scheduler), index.ts; wired into functions/src/index.ts.
  • firestore.rules — scoped speakers public read + revised header/merge note.
  • src/lib/firebase.tsgetFirestoreDb() on the App-Check app.
  • src/lib/speakers.ts — browser-safe types, linkType→icon map (globe fallback), initials, defensive doc mapper.
  • src/components/Speakers.tsx + Speakers.module.scss — island + noir card styles.
  • src/pages/speakers.astro + speakers.scss — page shell, hero, <noscript>.
  • src/components/Menu.astro — add Speakers, remove 2025 Edition.

Deploy prerequisites (maintainer)

firestore.rules is not wired into firebase.json (shared project) — the rule change must be merged into the Firebase console by hand, scoped to speakers only.

  1. firebase functions:secrets:set SESSIONIZE_ENDPOINT_ID — a JSON All data endpoint id (not an embed id; an embed returns HTML).
  2. Merge the speakers read block into the console ruleset — do not widen invoices/default.
  3. Trigger one refreshSessionize run to populate before launch (else the page shows "Lineup announced soon").

Review note: onSnapshot (vs one-shot getDocs) is a deliberate choice for parity with Tickets.tsx; cost is negligible at ~30–60 docs.

ryzizub and others added 26 commits July 7, 2026 23:41
Add functions/src/speakers/ domain — daily onSchedule refreshSpeakers
mirrors the Sessionize Speakers view into Firestore `speakers` with a
guarded upsert/delete batch. Includes the plan + brainstorm docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scope a public read on the `speakers` collection in firestore.rules
(invoices/default stay deny-all), add getFirestoreDb() on the App-Check
app, and add browser-safe speaker types + kind→icon map + helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add /speakers page with a live Firestore-backed Speakers island (noir
colour-bleed cards mirroring /team, loading/empty/error states, monogram
+ onError + tagline/clamp fallbacks), page styles, a <noscript> fallback,
and a Speakers nav entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard against duplicate Sessionize speaker ids, dedupe links, strip URL
userinfo + re-check scheme at the render boundary, add a fetch timeout,
drop a redundant delete-guard branch, and document the batch 500-op cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Point the sync at the /view/All endpoint (object payload) and store the
complete Sessionize speaker record (bio, sessions, categories, Q&A,
isTopSpeaker) alongside processed links/order. Rename the domain
speakers/ -> sessionize/ and refreshSpeakers -> refreshSessionize; the
browser still reads the `speakers` collection unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feature is implemented and shipped; drop the planning artifacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deployed refreshSessionize 400'd because the SESSIONIZE_ENDPOINT_ID
secret held a full URL, which the code appended /view/All to. Add
parseEndpointId to extract the bare id from a URL or bare value, try the
All-data view then fall back to the Speakers view, and accept either the
object or bare-array payload shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the /team-cloned colour-bleed mugshot cards with a unique
speaker treatment: full-bleed grayscale portrait, overlaid caption +
gradient scrim, lineup index number, and a restrained hover (zoom +
desaturation lift + red edge). Same film-noir system and brand fonts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Append a dashed "More speakers announced soon" tile to the /speakers
grid, and add an auto-rotating speaker spotlight panel to the home page
(SpeakersTeaser island) that cycles the live lineup and links to
/speakers. The teaser renders nothing until data loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the card caption into a panel below the portrait so text no longer
covers the image, and make each card a button that opens an accessible
detail dialog (portrait, bio, talks, social links) with focus trap, Esc
/ backdrop close, scroll lock, and focus restore. Expose bio + sessions
on the client Speaker type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch the home SpeakersTeaser to client:load — it renders nothing until
Firestore resolves, so a client:visible observer never fired on the
zero-height placeholder and it never hydrated. Also join speaker session
ids against the All payload's top-level sessions[] so the detail dialog
shows talk titles (the All view gives a speaker only session ids).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Raise the detail dialog overlay above the fixed site header (z 10050 >
  10001) so the close button is no longer covered by the top bar.
- Equalize speaker card heights: stretch cards to row height, reserve
  two-line name/tagline slots, and bottom-align the "View profile" CTA.
- Replace the rotating home spotlight with a grayscale mugshot wall that
  shows the whole lineup at once (noir suspects board) + an "All" tile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lineup changes often; don't cite a fixed number.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give the speaker detail dialog a fixed desktop height (min(78vh, 620px))
so it doesn't resize between speakers with longer/shorter bios; the
content column scrolls. Mobile keeps content height.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the generic grayscale boxes with an evidence-wall dossier look:
red corner crop-brackets, a scanline + red-spotlight grain overlay,
higher-contrast duotone portraits, and a red-ruled file plate for names.
Applied to both the home lineup wall and the /speakers grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the TV/scanline layer from the home lineup tiles (and the "ALL"
tile); keep the crop-brackets, red spotlight tint, and name plates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"On the bill" read awkwardly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show 5 mugshots at a time (down from 11) and rotate the visible set
through the full roster on a timer (crossfade, pause on hover, respects
reduced motion). Drop the "+N" tile for a centered "See all speakers"
link below the wall.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cap the wall at 4 tiles in one fixed row at every width (no wrapping on
phone/tablet), and centre it at max-width 880px so wide desktops get
left/right breathing room. Tighten spacing/name size on small screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the TV/scanline overlay and the 01/02 index from the speaker
grid cards; keep the crop-brackets, red spotlight tint, and name plate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align the speaker card name to .crew-name (Bebas 1.55rem / lh 1 / 0.04em)
and the tagline to .crew-role (JetBrains Mono 0.75rem / 0.16em / uppercase
/ lh 1.4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the inset margin around the red-tinted caption so it aligns
edge-to-edge with the photo instead of floating with a gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve each session's abstract (not just its title) from the All
payload's sessions[] during sync, store it on the speaker doc, and render
the title + description in the modal's Talks section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audit waited for 'networkidle', which never fires while the Firestore
speakers wall / RTDB tickets listeners hold a channel open — timing out
page.goto. Load the DOM, then wait for idle only briefly and fall through.
Add /speakers to the audited paths, and give the "all speakers" link a
visible focus ring instead of removing its outline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ryzizub ryzizub marked this pull request as ready for review July 9, 2026 06:53
Load the first row of speaker card images eagerly (fetchpriority high)
instead of lazy, clearing Astro's above-the-fold loading audit and
improving LCP on the lineup page. Later rows stay lazy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ryzizub ryzizub merged commit 84f7228 into 2026 Jul 9, 2026
2 checks passed
@ryzizub ryzizub deleted the claude/speakers-session-brainstorm-4dd1d4 branch July 9, 2026 07:04
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.

1 participant