From b5afc8885726ac977989e1ef58002f528698a4ae Mon Sep 17 00:00:00 2001 From: leahpeker Date: Thu, 23 Jul 2026 10:10:03 -0400 Subject: [PATCH 1/3] fix(events): collapse rsvp picker into single button (Issue 1133) Replace the 3-button going/maybe/can't-go picker on the event detail page with one 'rsvp' button that opens the same RsvpBox modal, since the modal already lets you pick a status. The already-RSVP'd state (single status/edit button) is unchanged. Also drops the now-unused 'prominent' prop from RsvpStatusPicker. --- .../components/ui/RsvpStatusPicker.test.tsx | 15 ---------- .../src/components/ui/RsvpStatusPicker.tsx | 5 +--- .../src/screens/events/RsvpSection.test.tsx | 30 +++++++++++++------ frontend/src/screens/events/RsvpSection.tsx | 18 +++++------ 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/ui/RsvpStatusPicker.test.tsx b/frontend/src/components/ui/RsvpStatusPicker.test.tsx index e05bade0f..a50ebef65 100644 --- a/frontend/src/components/ui/RsvpStatusPicker.test.tsx +++ b/frontend/src/components/ui/RsvpStatusPicker.test.tsx @@ -51,21 +51,6 @@ describe('RsvpStatusPicker', () => { expect(screen.getByRole('button', { name: 'join the waitlist' })).toBeInTheDocument(); }); - it('renders default-size pills unless prominent is set', () => { - render(); - const pill = screen.getByRole('button', { name: "i'm going" }); - expect(pill).toHaveClass('h-10'); - expect(pill).not.toHaveClass('flex-1'); - }); - - it('renders bigger, full-width pills when prominent', () => { - render(); - const pill = screen.getByRole('button', { name: "i'm going" }); - expect(pill).toHaveClass('h-12'); - expect(pill).toHaveClass('flex-1'); - expect(pill).toHaveClass('text-base'); - }); - it('filters to only the given statuses', () => { render(); expect(screen.getByRole('button', { name: "i'm going" })).toBeInTheDocument(); diff --git a/frontend/src/components/ui/RsvpStatusPicker.tsx b/frontend/src/components/ui/RsvpStatusPicker.tsx index 346682432..ee2bd5be2 100644 --- a/frontend/src/components/ui/RsvpStatusPicker.tsx +++ b/frontend/src/components/ui/RsvpStatusPicker.tsx @@ -8,7 +8,6 @@ interface Props { disabled?: boolean; labelFor?: (status: RsvpInputStatus, defaultLabel: string) => string; statuses?: RsvpInputStatus[]; - prominent?: boolean; } export function RsvpStatusPicker({ @@ -17,7 +16,6 @@ export function RsvpStatusPicker({ disabled = false, labelFor, statuses, - prominent = false, }: Props) { const options = statuses ? RSVP_STATUS_LABELS.filter((p) => statuses.includes(p.status)) @@ -37,8 +35,7 @@ export function RsvpStatusPicker({ onSelect(p.status); }} className={cn( - 'inline-flex shrink-0 items-center justify-center rounded-full font-medium transition-colors disabled:cursor-not-allowed', - prominent ? 'h-12 flex-1 px-5 text-base' : 'h-10 px-4 text-sm', + 'inline-flex h-10 shrink-0 items-center justify-center rounded-full px-4 text-sm font-medium transition-colors disabled:cursor-not-allowed', active ? 'bg-brand-600 text-brand-on' : 'border-border-strong text-foreground-secondary hover:bg-background border', diff --git a/frontend/src/screens/events/RsvpSection.test.tsx b/frontend/src/screens/events/RsvpSection.test.tsx index d2a82b8fe..047ca63a3 100644 --- a/frontend/src/screens/events/RsvpSection.test.tsx +++ b/frontend/src/screens/events/RsvpSection.test.tsx @@ -67,29 +67,41 @@ beforeEach(() => { }); describe('RsvpSection — before RSVPing', () => { - it('opens the RSVP box when a pill is tapped (not yet RSVP’d)', () => { + it('opens the RSVP box when the rsvp button is tapped (not yet RSVP’d)', () => { renderSection(makeEvent({ myRsvp: null })); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: "i'm going" })); + fireEvent.click(screen.getByRole('button', { name: 'rsvp' })); expect(screen.getByRole('dialog', { name: /RSVP/i })).toBeInTheDocument(); }); - it('shows all three pills and no status line when I have not RSVP’d', () => { + it('shows a single rsvp button and no status line when I have not RSVP’d', () => { renderSection(makeEvent({ myRsvp: null })); - expect(screen.getByRole('button', { name: "i'm going" })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'maybe' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: "can't go" })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'rsvp' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: "i'm going" })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'maybe' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: "can't go" })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /edit RSVP/i })).not.toBeInTheDocument(); }); - it('shows "join the waitlist" instead of "i\'m going" when the event is full', () => { + it('shows "join the waitlist" instead of "rsvp" when the event is full', () => { renderSection(makeEvent({ maxAttendees: 2, attendingCount: 2, myRsvp: null })); expect(screen.getByRole('button', { name: 'join the waitlist' })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: "i'm going" })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'rsvp' })).not.toBeInTheDocument(); + }); + + it('opens the RSVP box defaulted to "going" when the rsvp button is tapped', () => { + renderSection(makeEvent({ myRsvp: null })); + + fireEvent.click(screen.getByRole('button', { name: 'rsvp' })); + + expect(screen.getByRole('button', { name: "i'm going" })).toHaveAttribute( + 'aria-pressed', + 'true', + ); }); }); @@ -271,7 +283,7 @@ describe('RsvpSection — comments in public manage vs member flows', () => { }), ); - fireEvent.click(screen.getByRole('button', { name: /going/i })); + fireEvent.click(screen.getByRole('button', { name: 'rsvp' })); expect(screen.getByTestId('rsvp-comment-field')).toBeInTheDocument(); }); }); diff --git a/frontend/src/screens/events/RsvpSection.tsx b/frontend/src/screens/events/RsvpSection.tsx index 994b1bc1b..328dc498f 100644 --- a/frontend/src/screens/events/RsvpSection.tsx +++ b/frontend/src/screens/events/RsvpSection.tsx @@ -5,7 +5,6 @@ import { useCancelPublicMyRsvp, useUpdatePublicMyRsvp } from '@/api/publicRsvp'; import { useRemoveRsvp, useSetRsvp } from '@/api/rsvp'; import { useAuthStore } from '@/auth/store'; import { Button } from '@/components/ui/Button'; -import { RsvpStatusPicker } from '@/components/ui/RsvpStatusPicker'; import { type Event, isRsvpInputStatus, @@ -198,15 +197,16 @@ function RsvpControls({ } return ( - { + onOpenCreate(RsvpStatus.Attending); + }} disabled={busy} - prominent - onSelect={onOpenCreate} - labelFor={(status, defaultLabel) => - status === RsvpStatus.Attending && atCapacity ? 'join the waitlist' : defaultLabel - } - /> + className="bg-brand-600 text-brand-on hover:bg-brand-700 mx-auto inline-flex h-12 min-w-28 items-center justify-center rounded-full px-5 text-base font-medium transition-colors disabled:opacity-60" + > + {atCapacity ? 'join the waitlist' : 'rsvp'} + ); } From 3ea824e086741b3a3cbb3e5470e74b93c835aa12 Mon Sep 17 00:00:00 2001 From: leahpeker Date: Fri, 24 Jul 2026 17:49:35 -0400 Subject: [PATCH 2/3] fix(e2e): update rsvp button selector after single-button collapse rsvp-member.spec.ts and live-updates.spec.ts still clicked "i'm going", the old 3-button picker's label. The member RsvpSection's primary action button is now a single "rsvp" button (or "join the waitlist" at capacity); rsvp-public-* specs are unaffected since they go through PublicRsvpForm/RsvpStatusPicker, a separate component this PR didn't touch. --- frontend/e2e/live-updates.spec.ts | 2 +- frontend/e2e/rsvp-member.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/e2e/live-updates.spec.ts b/frontend/e2e/live-updates.spec.ts index bbd744cf0..a73e07c54 100644 --- a/frontend/e2e/live-updates.spec.ts +++ b/frontend/e2e/live-updates.spec.ts @@ -39,7 +39,7 @@ test('member A comment appears live in member B open event view via SSE', async await pageB.goto(`/events/${event_id}`); const rsvpSectionA = pageA.getByLabel('rsvp'); - await rsvpSectionA.getByRole('button', { name: "i'm going" }).click(); + await rsvpSectionA.getByRole('button', { name: 'rsvp' }).click(); await pageA .getByRole('dialog', { name: 'rsvp' }) .getByRole('button', { name: 'confirm' }) diff --git a/frontend/e2e/rsvp-member.spec.ts b/frontend/e2e/rsvp-member.spec.ts index 18e189709..2e86cbda4 100644 --- a/frontend/e2e/rsvp-member.spec.ts +++ b/frontend/e2e/rsvp-member.spec.ts @@ -27,7 +27,7 @@ test('member rsvps to an event from event detail', async ({ page }) => { await expect(page.getByRole('heading', { name: event_title })).toBeVisible(); const rsvpSection = page.getByLabel('rsvp'); - await rsvpSection.getByRole('button', { name: "i'm going" }).click(); + await rsvpSection.getByRole('button', { name: 'rsvp' }).click(); const rsvpDialog = page.getByRole('dialog', { name: 'rsvp' }); await rsvpDialog.getByRole('button', { name: 'confirm' }).click(); From 0ad0816934e05ae5f5812d4c490ca491eb2e80f3 Mon Sep 17 00:00:00 2001 From: leahpeker Date: Fri, 24 Jul 2026 18:03:30 -0400 Subject: [PATCH 3/3] style: fix prettier formatting in RsvpStatusPicker --- frontend/src/components/ui/RsvpStatusPicker.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/components/ui/RsvpStatusPicker.tsx b/frontend/src/components/ui/RsvpStatusPicker.tsx index ee2bd5be2..88382b274 100644 --- a/frontend/src/components/ui/RsvpStatusPicker.tsx +++ b/frontend/src/components/ui/RsvpStatusPicker.tsx @@ -10,13 +10,7 @@ interface Props { statuses?: RsvpInputStatus[]; } -export function RsvpStatusPicker({ - value, - onSelect, - disabled = false, - labelFor, - statuses, -}: Props) { +export function RsvpStatusPicker({ value, onSelect, disabled = false, labelFor, statuses }: Props) { const options = statuses ? RSVP_STATUS_LABELS.filter((p) => statuses.includes(p.status)) : RSVP_STATUS_LABELS;