Skip to content

feat: events — collection, enrollment, calendar, embeddable blocks - #98

Merged
InnerDesires merged 14 commits into
mainfrom
claude/events-feature-df10f1
Sep 28, 2026
Merged

InnerDesires merged 14 commits into
mainfrom
claude/events-feature-df10f1

Conversation

@InnerDesires

@InnerDesires InnerDesires commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

What

A full events feature that mirrors the courses/enrollments architecture.

Data model

  • events collection — drafts/versions with autosave + scheduled publish, localized title/description/address, cover (media), start/end datetime, locationType: local | virtual with conditional fields (address + map link vs meeting link), optional capacity, auto-stamped publishedAt. Public reads are published-only (authenticatedOrPublished); admin-only writes.
  • event-enrollments collection — unique user×event (hook check + DB unique index), rate-limited creates (event-enroll-create, 30/10 min), non-admin REST creates are bound to the requesting user before the duplicate check. Business rules enforced in beforeValidate for users and server actions alike (admins may bypass for manual sign-ups): event must be published, not finished, and have free seats. Owners may cancel their own registration (no forgeable progress here — mirrors Likes); enrolledAt server-stamped.
  • Cascades: deleting an event or user removes dependent registrations (NOT NULL + ON DELETE SET NULL FK shape requires it).

Zoom integration decision

A real embedded Zoom client needs a Meeting SDK app, a backend JWT-signing endpoint and (since March 2026) ZAK/OBF app-authorization for external participants — heavy infrastructure and credential liability for marginal UX gain. Instead the meeting link is a first-class field rendered as a platform-aware join card (Zoom / Google Meet / YouTube accents), and it is treated as a perk of registration:

  • field access strips it from unauthenticated API reads,
  • pages fetch with overrideAccess: false, so it never enters the shared ISR HTML,
  • registered users receive it after hydration via the getEventJoinInfo server action,
  • the public .ics endpoint and Google Calendar links use the event page URL as the location for virtual events, so calendar files don't leak it either.

Frontend (Iron Energy theme)

  • /events — ISR (300 s) page whose upcoming/past split happens client-side against the live clock (no stale boundary), with tabs, a featured next-event card with live countdown, date-tile cards echoing the homepage calendar motif, and enrolled badges fetched client-side so the page stays statically cacheable.
  • /events/[slug] — cover hero, format/seats badges, date-time summary, participants count, enroll/unenroll action bar (login redirect for guests, "no seats" state, confirm-on-cancel), location card with map link (offline) or join card (online), add-to-calendar (Google + .ics).
  • Profile — "Мої події" section listing upcoming registered events.
  • eventsBlock — embeddable in posts (Lexical BlocksFeature) and pages (layout blocks), populated by upcoming events or a hand-picked selection; renders published-only via access-controlled re-fetch.
  • Events are indexed by the search plugin (incl. locale backfill).

Migration

20260815_120000_events — hand-written idempotent DDL mirroring drizzle push (incl. unprefixed compound index name user_event_idx, search_rels/pages_rels/_pages_v_rels columns). Rehearsed on a temporary fork of ci-base: fresh up ✓, down ✓, re-up ✓; re-run on the pushed dev schema is a clean no-op.

Tests

  • 18 new integration tests (events, event-enrollments): publish validation (virtual needs link, local needs address, endDate > startDate), draft invisibility, meeting-link field access, non-admin write rejection, cascade delete, duplicate/draft/finished/capacity enrollment rejections, ongoing-event enrollment, user binding, adminOrOwn read matrix, owner-delete vs foreign-delete, owner-update rejection. Full suite: 141/141 passing.
  • e2e smoke spec: locale titles, detail page, guest sign-in prompt, .ics response.

Verified in browser (dev branch)

Listing tabs/featured/countdown (uk + en), enroll → enrolled badge + Zoom join card with link + live seats decrement, unenroll → back to enroll state, profile section, admin list view, .ics download.

Notes

  • The home-calendar global still powers the homepage "Найближчі зміни" section — it now overlaps with this collection and is worth migrating to events in a follow-up.
  • Events are deliberately not exposed via the MCP plugin yet.

🤖 Generated with Claude Code

Preview admin login fix

The preview branch URL is now included in Better Auth's trusted origins and Payload CORS. Preview auth uses the branch URL even if a production base URL override is configured, and the browser calls its own origin. This fixes INVALID_ORIGIN when signing in through the PR's stable Vercel preview link.

Event page UX, time, interactions, and SEO

  • Reworked the event detail into a compact responsive hero and schedule sidebar; simplified the join card and cancellation control.
  • Display event dates and day tiles in Europe/Kyiv with a visible Kyiv time label, while calendar files keep UTC instants.
  • Added event targets to comments and likes, event comment links in profiles, deletion cleanup, generated Payload types, and the idempotent 20260928_120000_event_interactions enum migration.
  • Added sharing controls, canonical and language alternates, Open Graph and Twitter metadata, and Event JSON-LD. Preview links resolve to the preview branch URL.
  • Added date, interaction, SEO, and UI checks.

The long-lived Vercel preview database needs the two enum additions before event comments and likes can be exercised there. Previews deliberately skip migrations; production applies the migration during deployment.

Comments and likes stuck in skeleton (500s on event pages)

  • Root cause: event comments/likes add events to the target_collection enums, but previews never run migrations, so the preview DB still had posts,courses(,comments). Every event-page query failed with invalid input value for enum enum_likes_target_collection: "events", the server action returned 500, and the client had no failure path, so the skeleton pulsed forever. The preview DB has been given the two enum values (same idempotent statements as 20260928_120000_event_interactions); production gets them from the migration on deploy.
  • Resilience fix: CommentsSection now shows a load error with a retry button, LikeButton falls back to a usable button (and reverts the optimistic flip if a toggle throws), instead of pulsing forever when an action rejects. New i18n keys commentsLoadError / commentsRetry (uk + en).
  • Tests: tests/e2e/comments-resilience.e2e.spec.ts forces the server actions to return 500 and asserts the skeleton clears and retry works. Fixed the JSON-LD assertion in events.e2e.spec.ts (a <script> has no rendered text, so toContainText always saw an empty string).
  • Merged main (admin docs, PR feat: двотрекова документація в адмінці (/admin/docs) #99) into the branch. Full integration suite: 22/22 files passing.

🤖 Generated with Claude Code

Code-review fixes and redesign alignment

  • meetingLink is now readable by admins only over REST (it was readable by any signed-in user); enrolled users still get it through getEventJoinInfo.
  • Registrations take a per-event transaction advisory lock before the capacity check, so concurrent sign-ups cannot exceed capacity (new concurrency test fails without the lock).
  • The events block in "selected manually" mode no longer falls back to upcoming events; the like button shows a neutral retry state instead of a made-up 0; event changes also revalidate the homepage, CMS pages and posts that can embed the block; events get a search type label; the delete cascade pages through comments; the event lookup is cached per render; unused i18n keys removed.
  • Not changed: the preview-specific auth/URL configuration from the other session (needs its own PR).
  • Merged main with the homepage redesign (feat: platform-first homepage redesign, abstract lines, full partners grid #100). The event detail header now uses the redesign's glow, abstract lines and accent line, and /events picks up the accent line through PageHead. No rails are used anywhere.

🤖 Generated with Claude Code

- events collection (drafts, uk/en locales, local/virtual location, cover,
  capacity) with published-only public reads; meeting link field-gated to
  authenticated reads and served to registered users via a server action
- event-enrollments collection: unique user×event, rate-limited creates,
  user binding on REST, published/not-finished/capacity rules in hooks,
  owner-cancellable, cascaded from events and users deletes
- frontend: /events with upcoming/past tabs, featured next-event card with
  live countdown; event page with enroll/unenroll, seats left, address/map
  for offline and a platform-aware join card for online events; add to
  calendar via Google link and public .ics endpoint (meeting link never
  leaks into static HTML or calendar files)
- eventsBlock embeddable in posts (Lexical) and pages (layout), upcoming or
  hand-picked selection
- profile shows upcoming registered events; events indexed in search
- idempotent migration rehearsed on a ci-base fork (up, down, re-up)
- 18 integration tests + e2e smoke specs
@vercel

vercel Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
learning-platform Ignored Ignored Preview Sep 28, 2026 9:54pm UTC

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Playwright Report

✅ E2E passed — job logs (run attempt 1)

…on fails

The comment list and like button fetched via server actions with no
failure path, so any rejected action left the skeleton pulsing forever.
Comments now show an error with a retry, the like button falls back to a
usable state, and a toggle failure reverts the optimistic flip. Adds an
e2e test that forces the actions to return 500.
A script element has no rendered text, so toContainText always saw an
empty string even though the page emits the correct Event schema.
The Google Meet accent was a teal that read as a different state next to
the green registered badge. Meet now uses the exact success green and the
join button uses the brand orange like other primary actions. The share
row on event pages no longer sits in a bordered card.
- meetingLink is readable by admins only over REST; enrolled users still get it via getEventJoinInfo
- serialize registrations per event with a transaction advisory lock so capacity cannot be exceeded
- events block in selection mode no longer falls back to upcoming events
- like button shows a neutral retry state instead of made-up 0 likes when the lookup fails
- event changes also revalidate the homepage, CMS pages and posts that can embed the block
- search results for events get a type label
- event delete cascade pages through comments instead of capping at 10000
- cache the event lookup per render and drop the redundant router.refresh
- remove unused i18n keys
Use the redesign's blue glow over the void gradient, the abstract hero
lines and an accent line under the title. No rails are introduced.
@InnerDesires
InnerDesires merged commit b0386ba into main Sep 28, 2026
5 checks passed
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