fix(tours): give an actual track its own mark and a winter tile its label's icon

Footprints stood for a snowshoe hike, an actual track and a walk's moving
time at once, with the snowshoe pill and the track pill side by side in a
tour's head. An actual track now goes by the done mark on the list row,
in the head and on a recording's tile, and a winter tile's corner takes
the icon its head pill shows.
This commit is contained in:
Maurice
2026-10-20 16:46:08 +02:00
parent 2ee4ea1e65
commit 6b1488994f
8 changed files with 38 additions and 12 deletions
@@ -321,6 +321,7 @@ describe('TourDetailDialog', () => {
render(<TourDetailDialog {...defaultProps} tour={{ ...tour, has_waypoints: false, origin: 'gpx', activity_of: 7 }} />)
expect(screen.getByTestId('tour-detail-track-of')).toHaveTextContent('Actual track of Lake loop')
expect(screen.getByTestId('tour-detail-track-of').querySelector('svg.lucide-check-check')).not.toBeNull()
})
it('ACT-02: an actual track stands beside the plan the head shows, and only the map detail offers to compare it there', async () => {
@@ -4,6 +4,7 @@ import {
ArrowDown,
ArrowUp,
Check,
CheckCheck,
ChevronDown,
ChevronUp,
Clock,
@@ -13,7 +14,6 @@ import {
FileDown,
FileImage,
FileText,
Footprints,
Loader2,
Minus,
Navigation,
@@ -301,7 +301,7 @@ function TourHeadPills({
{documentsName && (
<Tooltip label={t('tours.activity.isTrackHint')}>
<span data-testid="tour-detail-track-of" className={`${HEAD_PILL} max-w-[220px]`} style={fs(10.5)}>
<Footprints size={11} strokeWidth={2} className="flex-none text-success" />
<CheckCheck size={11} strokeWidth={2} className="flex-none text-success" />
<span className="min-w-0 truncate">{t('tours.activity.ofTour', { name: documentsName })}</span>
</span>
</Tooltip>
@@ -11,6 +11,9 @@ describe('TourActivityBadge (ACT-01)', () => {
<TourActivityBadge tour={{ activity_tracks: [{ id: 1, track_place_id: 9, assignment_id: null }] }} />
);
expect(screen.getByTestId('tour-activity-badge')).toHaveAttribute('aria-label', 'tours.activity.badgeHint');
// The done mark, never footprints, which name a snowshoe hike and a walk's moving time.
expect(screen.getByTestId('tour-activity-badge').querySelector('svg.lucide-check-check')).not.toBeNull();
expect(screen.getByTestId('tour-activity-badge').querySelector('svg.lucide-footprints')).toBeNull();
rerender(<TourActivityBadge tour={{ activity_tracks: [], activity_of: 4 }} />);
expect(screen.getByTestId('tour-activity-badge')).toHaveAttribute('aria-label', 'tours.activity.isTrackHint');
@@ -102,6 +102,10 @@ describe('TourActivitySection (ACT-01)', () => {
expect(screen.getByTestId('tour-activity-compare-time')).toHaveTextContent('tours.activity.noTime');
expect(screen.getByTestId('tour-activity-day')).toHaveTextContent('tours.activity.noDay');
expect(screen.queryByText('tours.activity.recordedOn')).toBeNull();
// The recording's tile goes by the done mark every actual track has, never footprints.
const head = screen.getByTestId('tour-activity').querySelector('header') as HTMLElement;
expect(head.querySelector('svg.lucide-check-check')).not.toBeNull();
expect(head.querySelector('svg.lucide-footprints')).toBeNull();
});
it('FE-ACT-SECTION-003: someone who may change the tour links, changes and unlinks; the dialog opens when asked', () => {
@@ -1,5 +1,5 @@
import type { TourActivity, TourListItem } from '@trek/shared';
import { CalendarDays, Clock, CloudOff, FileDown, Footprints, Plus, RefreshCw, Unlink } from 'lucide-react';
import { CalendarDays, CheckCheck, Clock, CloudOff, FileDown, Plus, RefreshCw, Unlink } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from '../../../i18n';
import { useSettingsStore } from '../../../store/settingsStore';
@@ -29,11 +29,16 @@ interface PlanSide {
comparison: ActivityComparisonState | null;
}
/** The tile of a recording: footprints, with a small dot at its foot that says it was recorded. */
/**
* The tile of a recording: the done mark every actual track goes by (on the
* list row, in the tour's head and here), with a small dot at its foot that
* says it was recorded. Never footprints, which name a snowshoe hike and a
* walk's moving time.
*/
function RecordedTile() {
return (
<span className="relative grid h-9 w-9 flex-none place-items-center rounded-full bg-surface-secondary text-content-secondary">
<Footprints size={15} strokeWidth={2} aria-hidden="true" />
<CheckCheck size={15} strokeWidth={2} aria-hidden="true" />
<span
aria-hidden="true"
className="absolute -bottom-0.5 -end-0.5 h-3 w-3 rounded-full bg-success ring-2 ring-surface-card"
+2 -2
View File
@@ -1,6 +1,6 @@
import { useId, useLayoutEffect, useRef, useState, type ReactNode } from 'react'
import type { TourListItem } from '@trek/shared'
import { AlertTriangle, CalendarDays, ChevronDown, Footprints, Info, Plus } from 'lucide-react'
import { AlertTriangle, CalendarDays, CheckCheck, ChevronDown, Info, Plus } from 'lucide-react'
import { createElement } from 'react'
import type { Day, DistanceUnit } from '../../types'
import type { RouteGeometryAnalysis } from '../../utils/routeGeometry'
@@ -134,7 +134,7 @@ export function TourActivityBadge({ tour }: { tour: Pick<TourListItem, 'activity
return (
<Tooltip label={label}>
<span data-testid="tour-activity-badge" aria-label={label} className="inline-flex flex-none">
<WhiteBadge className="text-success" icon={<Footprints size={10} strokeWidth={2.2} className="flex-none" />}>
<WhiteBadge className="text-success" icon={<CheckCheck size={10} strokeWidth={2.2} className="flex-none" />}>
{null}
</WhiteBadge>
</span>
@@ -4,7 +4,7 @@ import { TourDayRowAvatar } from '../TourDayRowFacts';
import { TourGradeTile } from '../tourParts';
import { WinterActivityPill, WinterCornerMark } from './winterMarks';
// FE-TOUR-WINTER-MARK-001 to FE-TOUR-WINTER-MARK-004
// FE-TOUR-WINTER-MARK-001 to FE-TOUR-WINTER-MARK-005
const hike = { tour_type: 'hike' as const, max_hiking_difficulty: 2, wanderer_ref: null, has_waypoints: true };
@@ -40,4 +40,16 @@ describe('winter marks (HIK-01)', () => {
expect(screen.getByTestId('plan-tour-icon')).toHaveTextContent('T2');
expect(screen.getByTestId('tour-winter-mark')).toHaveStyle({ width: '22px' });
});
it('FE-TOUR-WINTER-MARK-005: the corner mark goes by the same icon as the head pill of its label', () => {
for (const activity of ['winter_hike', 'snowshoe'] as const) {
const icon = activity === 'snowshoe' ? 'lucide-footprints' : 'lucide-snowflake';
const mark = render(<WinterCornerMark activity={activity} />);
expect(mark.container.querySelector(`svg.${icon}`)).not.toBeNull();
mark.unmount();
const pill = render(<WinterActivityPill activity={activity} className="head-pill" />);
expect(pill.container.querySelector(`svg.${icon}`)).not.toBeNull();
pill.unmount();
}
});
});
@@ -12,9 +12,10 @@ export const WINTER_ACTIVITY_ICON: Record<WinterActivity, LucideIcon> = {
};
/**
* HIK-01: a hike's winter label on its round tile, a frosted snowflake badge
* at the tile's top corner, the way the tile carries where a tour came from at
* its foot. Its tooltip names the label; nothing is drawn for a hike without one.
* HIK-01: a hike's winter label on its round tile, a frosted badge at the
* tile's top corner with the label's own icon, the one its head pill shows,
* the way the tile carries where a tour came from at its foot. Its tooltip
* names the label; nothing is drawn for a hike without one.
*/
export function WinterCornerMark({
activity,
@@ -28,7 +29,7 @@ export function WinterCornerMark({
if (!activity) return null;
return (
<TourCornerMark
icon={Snowflake}
icon={WINTER_ACTIVITY_ICON[activity]}
label={t(`tours.winter.activity.${activity}`)}
size={size}
testId="tour-winter-mark"