From b5ebffa115cd3918eec6e161d330a97f0305d7a9 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 26 Jul 2026 12:32:24 +0200 Subject: [PATCH] fix(dashboard): keep a trip the hero fell back to in the grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero picks the trip that is running, else the next one coming up, else just trips[0] — and that pick was then removed from the grid below. The first two cases are deliberate: the trip is already on screen, showing it again right underneath would be noise. The trips[0] fallback is a different thing. It fires precisely when nothing qualifies, so it borrows a trip that belongs in the grid, and removing it there leaves the user with a hero and an empty list. Someone whose trips are all finished sees "No trips yet" under a card of the trip they just came back from. Split the two: only a trip that earned the hero is taken out of the grid. Undated trips were caught by this too, and worse — getTripStatus returns null rather than 'past' for them, so a lone dateless trip appeared in no tab at all. The empty state was the second half of the report. It equated "planned is empty" with "you have no trips" and told people to create their first one. Gate it on there being no trip at all, which is the condition the mobile dashboard already uses. Mobile shares useDashboard, so it is fixed along with the desktop. Closes #1706 --- client/src/pages/DashboardPage.test.tsx | 59 +++++++++++++++++++++- client/src/pages/DashboardPage.tsx | 5 +- client/src/pages/dashboard/useDashboard.ts | 12 +++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/client/src/pages/DashboardPage.test.tsx b/client/src/pages/DashboardPage.test.tsx index d87279808..88f93cc49 100644 --- a/client/src/pages/DashboardPage.test.tsx +++ b/client/src/pages/DashboardPage.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { render, screen, waitFor } from '../../tests/helpers/render'; +import { render, screen, waitFor, within } from '../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; import { server } from '../../tests/helpers/msw/server'; @@ -943,4 +943,61 @@ describe('DashboardPage', () => { expect(useSettingsStore.getState().settings.dashboard_timezones).toBeUndefined(); }); }); + + describe('FE-PAGE-DASH-035: A trip the hero fell back to still appears in the grid (#1706)', () => { + const onlyTrips = (trips: unknown[]) => + server.use( + http.get('/api/trips', ({ request }) => { + const url = new URL(request.url); + if (url.searchParams.get('archived')) return HttpResponse.json({ trips: [] }); + return HttpResponse.json({ trips }); + }), + ); + // The grid, not the hero — the hero renders the same title and would mask the bug. + const grid = () => document.querySelector('.trips') as HTMLElement; + + it('lists a finished trip under Completed when it is the only one', async () => { + onlyTrips([buildTrip({ title: 'Lisbon 2025', start_date: '2025-05-01', end_date: '2025-05-08' })]); + const user = userEvent.setup(); + render(); + + await user.click(screen.getByText('Completed')); + + await waitFor(() => expect(within(grid()).getByText('Lisbon 2025')).toBeInTheDocument()); + }); + + it('lists a dateless trip under Planned when it is the only one', async () => { + onlyTrips([buildTrip({ title: 'Someday Iceland', start_date: null, end_date: null })]); + render(); + + await waitFor(() => expect(within(grid()).getByText('Someday Iceland')).toBeInTheDocument()); + expect(screen.queryByText('No trips yet')).not.toBeInTheDocument(); + }); + + it('does not claim "No trips yet" when the only trip is simply finished', async () => { + onlyTrips([buildTrip({ title: 'Lisbon 2025', start_date: '2025-05-01', end_date: '2025-05-08' })]); + render(); + + await waitFor(() => expect(screen.getAllByText('Lisbon 2025').length).toBeGreaterThan(0)); + expect(screen.queryByText('No trips yet')).not.toBeInTheDocument(); + }); + + it('still says "No trips yet" for an account with no trips at all', async () => { + onlyTrips([]); + render(); + + await waitFor(() => expect(screen.getByText('No trips yet')).toBeInTheDocument()); + }); + + it('still keeps a running trip out of the grid, so the hero does not double up', async () => { + onlyTrips([ + buildTrip({ title: 'Paris Adventure', start_date: '2026-07-01', end_date: '2026-07-10' }), + buildTrip({ title: 'Lisbon 2025', start_date: '2025-05-01', end_date: '2025-05-08' }), + ]); + render(); + + await waitFor(() => expect(screen.getAllByText('Paris Adventure').length).toBeGreaterThan(0)); + expect(within(grid()).queryByText('Paris Adventure')).not.toBeInTheDocument(); + }); + }); }); diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index 1120ba024..04f6a4279 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -220,7 +220,10 @@ function DashboardPageDesktop(): React.ReactElement { /> )} - {gridTrips.length === 0 && tripFilter === 'planned' && !isLoading && !loadError && ( + {/* "No trips yet" only when there really are none — a user whose trips are + all finished has a hero, and telling them to create their first trip is + simply wrong (#1706). Same condition the mobile dashboard already uses. */} + {gridTrips.length === 0 && !spotlight && tripFilter === 'planned' && !isLoading && !loadError && (
diff --git a/client/src/pages/dashboard/useDashboard.ts b/client/src/pages/dashboard/useDashboard.ts index c0223b59a..478ec9b89 100644 --- a/client/src/pages/dashboard/useDashboard.ts +++ b/client/src/pages/dashboard/useDashboard.ts @@ -93,11 +93,17 @@ export function useDashboard() { } const today = new Date().toISOString().split('T')[0] - const spotlight = trips.find(t => t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) + // A trip the hero features on its own merit: one that is running, else the next + // one coming up. Only that one is taken out of the grid below, so the same trip + // isn't shown twice. + const featured = trips.find(t => t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) || trips.find(t => t.start_date && t.start_date >= today) - || trips[0] || null - const rest = spotlight ? trips.filter(t => t.id !== spotlight.id) : trips + // With neither, the hero still shows something rather than sitting empty — but + // that trip is only borrowed for the header and must stay in the grid, or a user + // whose trips are all finished (or undated) sees "No trips yet". #1706 + const spotlight = featured || trips[0] || null + const rest = featured ? trips.filter(t => t.id !== featured.id) : trips // Pull the spotlight trip's members + places so the boarding pass can show // real buddies and place thumbnails instead of placeholders.