From bda24ec639e2a80606592d2f8df2ce0c094a30c7 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 17 Oct 2026 15:27:51 +0200 Subject: [PATCH] fix(tours): description on its own line, fewer and whole badges in the tour list The tour list row puts the description between the name and the badges. The badges keep the three that matter most (time, then a paddle tour's check bench and distance, or a hike's distance and climbing) and only those that fit whole, so a narrow sidebar shows two instead of cutting one off. A check bench without obstacle data shows no pill in the list or the day plan; the checklist still names the gap. --- .../components/Tours/TourDayRowFacts.test.tsx | 37 +++++++++- .../src/components/Tours/TourDayRowFacts.tsx | 73 +++++++++++-------- client/src/components/Tours/TourListRow.tsx | 34 ++++----- .../components/Tours/ToursSidebar.test.tsx | 11 ++- 4 files changed, 100 insertions(+), 55 deletions(-) diff --git a/client/src/components/Tours/TourDayRowFacts.test.tsx b/client/src/components/Tours/TourDayRowFacts.test.tsx index 355404b2d..b8d7b309e 100644 --- a/client/src/components/Tours/TourDayRowFacts.test.tsx +++ b/client/src/components/Tours/TourDayRowFacts.test.tsx @@ -86,9 +86,14 @@ describe('PAD-09 the check bench on a tour row', () => { expect(pill.getAttribute('aria-label')).toContain('A checklist is no safety or legal clearance.'); }); - it('PCD-002: missing obstacle data reads as such in the pill, never as a count', () => { + it('PCD-002: an area without obstacle data shows no pill at all, never a count', () => { render(); - expect(screen.getByTestId('plan-tour-paddle-check')).toHaveTextContent('No data'); + expect(screen.queryByTestId('plan-tour-paddle-check')).not.toBeInTheDocument(); + }); + + it('PCD-002b: data that could not be read still shows, without a count', () => { + render(); + expect(screen.getByTestId('plan-tour-paddle-check')).not.toHaveTextContent('3'); }); it.each([false, true])('PCD-003: the day plan facts name the bench as one more fact (mobile=%s)', (mobile) => { @@ -111,3 +116,31 @@ describe('PAD-09 the check bench on a tour row', () => { expect(screen.queryByTestId('plan-tour-paddle-check')).not.toBeInTheDocument(); }); }); + +describe('the compact facts of a tour list row', () => { + const testIds = () => + [...document.querySelectorAll('[data-testid^="plan-tour"], [data-testid="trip-plan-tour-time"]')].map((el) => + el.getAttribute('data-testid') + ); + const timed = { ...tour, distance: 4.2, elevation_gain: 478, planned_duration_minutes: 50 }; + + it('PCD-005: a hike keeps time, distance and climbing, in that order', () => { + render(); + expect(testIds()).toEqual(['trip-plan-tour-time', 'plan-tour-distance', 'plan-tour-ascent']); + }); + + it('PCD-006: a paddle tour puts its check bench second and leaves out the climbing', () => { + render(); + expect(testIds()).toEqual(['trip-plan-tour-time', 'plan-tour-paddle-check', 'plan-tour-distance']); + }); + + it('PCD-007: the day plan still shows every fact', () => { + render(); + expect(testIds()).toEqual([ + 'trip-plan-tour-time', + 'plan-tour-distance', + 'plan-tour-ascent', + 'plan-tour-paddle-check', + ]); + }); +}); diff --git a/client/src/components/Tours/TourDayRowFacts.tsx b/client/src/components/Tours/TourDayRowFacts.tsx index 68428d1d0..bda028676 100644 --- a/client/src/components/Tours/TourDayRowFacts.tsx +++ b/client/src/components/Tours/TourDayRowFacts.tsx @@ -141,18 +141,25 @@ export function TourDayRowPill({ ); } -/** A paddle tour's check bench as one fact (PAD-09), or null for a hike or a tour listed without one. */ +/** + * A paddle tour's check bench as one fact (PAD-09), or null for a hike, a tour + * listed without one, or an area without obstacle data: a row has no room for + * a badge that says nothing, and the checklist itself still names the gap. + */ function paddleCheckOf(tour: TourListItem, t: Parameters[1]) { - return tour.tour_type === 'kayak' && tour.paddle_check ? paddleCheckFact(tour.paddle_check, t) : null; + const check = tour.tour_type === 'kayak' ? tour.paddle_check : null; + return check && check.data !== 'none' ? paddleCheckFact(check, t) : null; } /** * A tour's facts on its day-plan row, in the badges every other stop uses: the * time it takes (the planned total where there is one, otherwise the walking * time, the parts in the tooltip), how far, how much climbing and, for a paddle - * tour, its check bench. + * tour, its check bench. `compact` (the tour list) keeps the three that matter + * most, in that order: the time, then a paddle tour's check bench and its + * distance, or a hike's distance and climbing. */ -export function TourDayRowPills({ tour }: { tour: TourListItem }) { +export function TourDayRowPills({ tour, compact = false }: { tour: TourListItem; compact?: boolean }) { const { t } = useTranslation(); const unit = useSettingsStore((state) => state.settings.distance_unit) || 'metric'; const times = tourPlannedTimes(tour); @@ -169,35 +176,37 @@ export function TourDayRowPills({ tour }: { tour: TourListItem }) { : null, ].filter((part): part is string => part !== null); const check = paddleCheckOf(tour, t); - return ( - <> - {shownMinutes != null && ( - - )} - {tour.distance != null && ( - - )} - {tour.elevation_gain != null && ( - - )} - {check && } - + const time = shownMinutes != null && ( + ); + const distance = tour.distance != null && ( + + ); + const ascent = tour.elevation_gain != null && ( + + ); + const bench = check && ; + if (!compact) return <>{[time, distance, ascent, bench]}; + // A paddle tour's climbing is the river's fall, nothing to plan by; its check bench is. + return <>{tour.tour_type === 'kayak' ? [time, bench, distance] : [time, distance, ascent]}; } export default function TourDayRowFacts({ tour, mobile = false }: { tour: TourListItem; mobile?: boolean }) { diff --git a/client/src/components/Tours/TourListRow.tsx b/client/src/components/Tours/TourListRow.tsx index a54d87709..aa4ab40ce 100644 --- a/client/src/components/Tours/TourListRow.tsx +++ b/client/src/components/Tours/TourListRow.tsx @@ -47,7 +47,7 @@ function MapToggleButton({ toggle }: { toggle: NonNullable {tour.name} - {/* One line like a plan row's: the badges keep their place, the description gives way, - and what does not fit fades out instead of ending at a hard edge. */} + {tour.description && ( + + {tour.description} + + )} + {/* One line of whole badges, most important first: one that does not fit wraps onto a + second line the fixed height hides, so a narrow sidebar shows two and never half of one. */}
- + - {tour.description && ( - - {tour.description} - - )}
@@ -108,9 +108,9 @@ function TourRowActions({ action, mapToggle, selected }: Pick { expect(row).toHaveClass('bg-surface-selected') }) - it('RS-01: trails the description after the badges on one line, as a plan row does', () => { + it('RS-01: puts the description on its own line between the name and the badges', () => { const listed = { ...tour, description: 'A quiet path above the lake', website: 'https://www.komoot.com/tour/42', planned_duration_minutes: 95 } render() const row = screen.getByRole('option', { name: /Selected ridge walk/i }) const line = within(row).getByTestId('tour-metrics') - expect(line).toHaveClass('flex-nowrap', 'overflow-hidden') + expect(line).toHaveClass('flex-wrap', 'h-6', 'overflow-hidden') expect(line).toHaveTextContent('1 h 35 min') - expect(within(line).getByTestId('tour-description-preview')).toHaveTextContent('A quiet path above the lake') - expect(within(line).getByTestId('tour-description-preview')).toHaveClass('truncate') + const description = within(row).getByTestId('tour-description-preview') + expect(line).not.toContainElement(description) + expect(description).toHaveTextContent('A quiet path above the lake') + expect(description).toHaveClass('truncate') + expect(description.compareDocumentPosition(line) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() // The information link belongs to the detail, not the list row. expect(row).not.toHaveTextContent('Komoot') })