Skip to content

feat: add event list page#314

Open
jakehobbs wants to merge 2 commits intouse-nested-layoutfrom
jake/event-list
Open

feat: add event list page#314
jakehobbs wants to merge 2 commits intouse-nested-layoutfrom
jake/event-list

Conversation

@jakehobbs
Copy link
Member

@jakehobbs jakehobbs commented Mar 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Dedicated "All Events" and "All Coachings" list pages with filterable tables
    • "New Coaching" creation flow
    • URL-driven filter persistence for shareable event/coaching links
    • Sortable event/coaching lists with expandable details and CSV export capabilities
  • UI/Style

    • Enhanced loading states across event and coaching sections
    • Improved date picker with compact formatting
    • Visual sort indicators in tables

@jakehobbs jakehobbs requested a review from alexsapps as a code owner March 2, 2026 02:58
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR reorganizes event and coaching management by introducing URL-driven filtering via nuqs, creating new EventsPage and EventListTable components for displaying filterable event/coaching lists, adding API methods for event retrieval and deletion, and restructuring navigation to separate "All Events/Coachings" and "New Event/Coaching" routes.

Changes

Cohort / File(s) Summary
Navigation & Routing
shared/nav.json, frontend-v2/src/components/nav.tsx
Updated navigation menu: added "All Events" and "New Coaching" items; renamed "New Connection" to "All Coachings"; changed hrefs for event routes. Added sibling-aware active-state logic to nav dropdown items to prevent parent highlighting when exact sibling match exists.
Events List Pages
frontend-v2/src/app/(authed)/events/page.tsx, frontend-v2/src/app/(authed)/events/loading.tsx, frontend-v2/src/app/(authed)/events/new/...
Created new EventsListPage wrapper and EventsLoading component; renamed AttendancePage to NewEventPage and EventLoading to NewEventLoading; updated EventForm import path from ./event-form to ../event-form.
Coaching Pages
frontend-v2/src/app/(authed)/coaching/page.tsx, frontend-v2/src/app/(authed)/coaching/loading.tsx, frontend-v2/src/app/(authed)/coaching/new/..., frontend-v2/src/app/(authed)/coaching/[id]/...
Refactored coaching page structure: CoachingPage now renders CoachingListPage component; updated ContentWrapper sizing from "sm" to "xl"; created new NewCoachingPage, NewCoachingLoading, EditCoachingLoading components; updated EventForm import path in coaching/[id]/page.tsx.
Events Core Components
frontend-v2/src/app/(authed)/events/events-page.tsx, frontend-v2/src/app/(authed)/events/event-list-table.tsx, frontend-v2/src/app/(authed)/events/event-form.tsx
Introduced EventsPage with mode-driven filtering (events vs. connections), URL-synced state management via nuqs, data fetching with React Query, and deletion flow. Created EventListTable component with TanStack React Table for sortable, expandable rows and responsive card UI. Updated EventForm redirect path from /event to /events for non-connection events.
Coaching List Component
frontend-v2/src/app/(authed)/coaching/coaching-list-page.tsx, frontend-v2/src/app/(authed)/events/[id]/loading.tsx
Added CoachingListPage that renders EventsPage in "connections" mode; created EditEventLoading component for event detail page loading state.
API Layer & Dependencies
frontend-v2/src/lib/api.ts, frontend-v2/package.json
Added nuqs dependency (^2.8.9); extended ApiClient with getEventList() and deleteEvent() methods; introduced EventListItem, EventType, EventListParams types and API_PATH keys for event operations.
UI & Component Refinements
frontend-v2/src/components/ui/date-picker.tsx, frontend-v2/src/app/(authed)/users/user-table.tsx, frontend-v2/src/app/(authed)/interest/generator/generator-form.tsx, frontend-v2/src/app/providers.tsx
Enhanced date picker with shrink-0 on icon and truncate on label; compacted date format (PPP → PP). Replaced inline sort indicator logic with reusable SortIndicator component in user table. Removed loading state disable logic in generator form. Wrapped providers with NuqsAdapter for URL query state integration.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant EventsPage as EventsPage Component
    participant URL as URL State (nuqs)
    participant API as API Client
    participant EventListTable as EventListTable
    participant Database as Database

    User->>EventsPage: Open /events or /coaching
    EventsPage->>URL: Initialize filter state (date range, mode)
    User->>EventsPage: Adjust filters (name, activist, date, type)
    EventsPage->>URL: Update URL with new filter state
    EventsPage->>API: getEventList(filter params)
    API->>Database: Query events with filters
    Database-->>API: Event list response
    API-->>EventsPage: EventListItem[]
    EventsPage->>EventListTable: Pass events, mode, onDelete handler
    EventListTable-->>User: Render sortable/expandable table or cards
    User->>EventListTable: Click sort, expand row, or edit/delete
    EventListTable->>User: Show expandable details or navigate
    User->>EventListTable: Trigger delete action
    EventListTable->>EventsPage: onDelete callback
    EventsPage->>API: deleteEvent(eventId)
    API->>Database: Delete event
    Database-->>API: Confirm deletion
    API-->>EventsPage: Success response
    EventsPage->>API: Refetch event list (cache invalidation)
    API->>Database: Query updated events
    Database-->>API: Updated event list
    API-->>EventListTable: Refresh data
    EventListTable-->>User: Display updated list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • alexsapps

Poem

🐰 A rabbit hops through URLs so fine,
With nuqs as guide, the filters align,
Events and coachings now dance in a table,
Sortable, expandable—oh, how they're able!
From event to coaching, a path freshly trod,
This PR's refactor? Simply quite odd! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add event list page' clearly summarizes the main change—introducing a new event list page component. This aligns with the primary additions: EventsPage, EventListTable, events/page.tsx, and the corresponding navigation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jake/event-list

Comment @coderabbitai help to get the list of available commands and usage tips.

)
const [showFilters, setShowFilters] = useState(false)

// Dirty = committed filters differ from defaults (controls Reset button)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude wrote a bunch of comments again. not sure if necessary

@jakehobbs
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/app/`(authed)/events/[id]/loading.tsx:
- Line 7: The loading header currently uses the ambiguous title "Attendance";
update the JSX heading in the loading.tsx component (the <h1 className="text-3xl
font-bold"> element) to an edit-event-specific label such as "Editing event" or
"Editing event…" so the UI clearly reflects the event edit route during
transitions; keep the existing className and structure but replace the inner
text accordingly.

In `@frontend-v2/src/app/`(authed)/events/event-list-table.tsx:
- Around line 95-102: The icon-only delete Button (component named Button
rendering <Trash2 /> with onClick calling onDelete(row.original)) lacks an
accessible name; update both instances in event-list-table.tsx to provide an
explicit accessible label (e.g., add aria-label or aria-labelledby and/or a
title) that describes the action and the target item (for example "Delete event:
{row.original.title}" or similar derived from row.original), ensuring screen
readers announce the purpose; keep the visual icon-only appearance (do not add
visible text) and ensure the prop is applied to the same Button elements that
call onDelete.
- Around line 366-368: The attendee list uses key={name} in the
event.attendees.map which can collide for duplicate names; change the key to a
truly unique value (e.g., include the event id and the index or an attendee id
if available). Update the map callback (event.attendees.map((name) => ...)) to
accept the index or use an attendee object and set key to a composite like
`${event.id}-${index}-${name}` or attendee.id so each <li> has a stable, unique
key.

In `@frontend-v2/src/app/`(authed)/events/events-page.tsx:
- Around line 194-201: handleFilter is currently committing empty string date
values (formStart/formEnd) into setUrlParams which results in invalid
event_date_start/event_date_end being sent to getEventList; update the date
parameters so that when formStart or formEnd are empty strings they become null
— e.g. replace the current checks with logic that treats '' as null and only
sets start/end when the value is non-empty and different from
defaultParams.event_date_start/event_date_end (or use a small helper like
normalizeDateParam(value, default) to return null for '' or default), ensuring
setUrlParams receives null for cleared dates rather than ''.
- Around line 49-60: ActivistFilterInput currently calls useActivistRegistry()
directly which causes the registry hook to remount and re-sync whenever
ActivistFilterInput is mounted/unmounted via the showFilters toggle; lift the
hook out of the input component into the parent (e.g., the EventsPage component
that owns showFilters), call useActivistRegistry() there once, and pass the
resulting registry (and any relevant registry methods) into ActivistFilterInput
as props; update ActivistFilterInput to accept a registry prop instead of
calling useActivistRegistry() itself (apply the same lift to the other filter
components noted in the comment).

In `@frontend-v2/src/app/`(authed)/events/new/loading.tsx:
- Line 7: Update the heading text in the loading component so it matches the
/events/new route: in frontend-v2/src/app/(authed)/events/new/loading.tsx
replace the h1 content currently reading "Attendance" with "New event" (preserve
the existing className and structure in the default Loading component or
whichever function returns that JSX).

In `@frontend-v2/src/components/nav.tsx`:
- Around line 105-114: Replace the simple equality check page === item.page in
AdbNav.vue with the TSX active-state logic: compute siblingExactMatch by
checking item.items.some(sibling => sibling !== innerItem &&
sibling.href.startsWith('/v2') && pathname === sibling.href.substring(3)), then
compute isActive using navPath !== null && (pathname === navPath ||
(!siblingExactMatch && pathname.startsWith(navPath + '/'))); use that isActive
value for the active class/flag. Ensure you reference the same symbols used in
the TSX code: siblingExactMatch, innerItem, item.items, pathname, navPath, and
isActive.

In `@frontend-v2/src/lib/api.ts`:
- Around line 406-411: The deleteEvent method sends a mutating POST to
API_PATH.EVENT_DELETE without the X-CSRF-Token header; update deleteEvent to
include the same CSRF header used by other write methods (e.g., add headers: {
"X-CSRF-Token": this.csrfToken || this.getCsrfToken() } or whatever CSRF
accessor your class uses) when calling this.client.post(API_PATH.EVENT_DELETE, {
body, headers }) so the request matches the application's CSRF-protected write
paths.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b29ba2f and 260901f.

⛔ Files ignored due to path filters (1)
  • frontend-v2/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • frontend-v2/package.json
  • frontend-v2/src/app/(authed)/coaching/[id]/loading.tsx
  • frontend-v2/src/app/(authed)/coaching/[id]/page.tsx
  • frontend-v2/src/app/(authed)/coaching/coaching-list-page.tsx
  • frontend-v2/src/app/(authed)/coaching/loading.tsx
  • frontend-v2/src/app/(authed)/coaching/new/loading.tsx
  • frontend-v2/src/app/(authed)/coaching/new/page.tsx
  • frontend-v2/src/app/(authed)/coaching/page.tsx
  • frontend-v2/src/app/(authed)/events/[id]/loading.tsx
  • frontend-v2/src/app/(authed)/events/[id]/page.tsx
  • frontend-v2/src/app/(authed)/events/activist-registry.ts
  • frontend-v2/src/app/(authed)/events/activist-storage.ts
  • frontend-v2/src/app/(authed)/events/attendee-input-field.tsx
  • frontend-v2/src/app/(authed)/events/event-form.tsx
  • frontend-v2/src/app/(authed)/events/event-list-table.tsx
  • frontend-v2/src/app/(authed)/events/events-page.tsx
  • frontend-v2/src/app/(authed)/events/loading.tsx
  • frontend-v2/src/app/(authed)/events/new/loading.tsx
  • frontend-v2/src/app/(authed)/events/new/page.tsx
  • frontend-v2/src/app/(authed)/events/page.tsx
  • frontend-v2/src/app/(authed)/events/useActivistRegistry.ts
  • frontend-v2/src/app/(authed)/interest/generator/generator-form.tsx
  • frontend-v2/src/app/(authed)/users/user-table.tsx
  • frontend-v2/src/app/providers.tsx
  • frontend-v2/src/components/nav.tsx
  • frontend-v2/src/components/ui/date-picker.tsx
  • frontend-v2/src/lib/api.ts
  • shared/nav.json

setSelectedIndex(-1)
}

// Handles ArrowUp, ArrowDown, and Escape. Callers handle Enter/Tab themselves.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol this is kinda funny

@@ -0,0 +1,371 @@
'use client'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no prefetching on this page b/c the way we do the default date filter is a little weird. might need to make changes on backend later to enable this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be nice to add this as a code comment

Copy link
Collaborator

@alexsapps alexsapps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments round 1 part 1. didn't read event table and events page yet but looks good so far.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the directory name be plural, "coachings"?

<CalendarIcon className="mr-2 h-4 w-4 shrink-0" />
<span className="truncate">
{value ? format(value, 'PP') : placeholder}
</span>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wanna document/explain this change in the components/README?

.array(z.string())
.nullable()
.transform((v) => v ?? []),
attendee_emails: z
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should maybe be a separate change but i wanna change it so attendance takers don't get access to anyone's email just by adding their name to attendance.

export type EventListMode = 'events' | 'connections'

const EVENT_TYPES: { value: EventType; label: string }[] = [
{ value: 'noConnections', label: 'All' },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this "noConnections"? maybe add a comment or rename it to "All"?

Comment on lines +103 to +115
const navPath = innerItem.href.startsWith('/v2')
? innerItem.href.substring(3)
: null
const siblingExactMatch = item.items.some(
(sibling) =>
sibling !== innerItem &&
sibling.href.startsWith('/v2') &&
pathname === sibling.href.substring(3),
)
const isActive =
navPath !== null &&
(pathname === navPath ||
(!siblingExactMatch && pathname.startsWith(navPath + '/')))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this logic be moved above to keep layout separate from navigation logic?

maybe like

let childrenItems = null;
if (isExpanded) {
  childrenItems = item.items.map(item => {navPath:..., siblingExactMatch:..., isActive:...});
}

then later

return ... {childrenItems && <div> ... {childrenItems.map(...)} }

<NuqsAdapter>
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay. hope it doesn't cover up nextjs on the bottom left.

label,
className,
}: {
registry: ReturnType<typeof useActivistRegistry>['registry']
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could just be registry: ActivistRegistry ?

]

// Activist autocomplete input backed by the activist registry
function ActivistFilterInput({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

big enough to be its own file imo

Copy link
Collaborator

@alexsapps alexsapps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

round 1 pt 2, got more to look at still

className?: string
}) {
const { suggestions, selectedIndex, onInputChange, onSelect, onKeyDown } =
useSuggestions((v) => registry.getSuggestions(v))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this gets me pretty confused, calling a hook that returns 2 outputs and 3 callbacks with names that overlap with this function's callbacks, and some of the outputs forwarded to while others are used directly.

how about a component that wraps all of these concerns into one place?
#315

alternatively i was thinking about monitoring key presses in this component and dispatching to methods on with a ref, but codex told me that's not idiomatic so i didn't try it, and anyway it might not solve the problem as well as a dedicated component.

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