From d8bd6ab9e2f885baf9193e3b3cb5e4ddcc2da446 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 22 Oct 2026 16:21:20 +0200 Subject: [PATCH] fix(ci): typed card points, the managed take route, and the failing card tests and ratchets --- .../Assistant/cards/EntityActions.test.tsx | 6 ++- .../cards/budget/ExpensesCard.test.tsx | 24 +++++------ .../cards/budget/PaymentsCard.test.tsx | 13 ++---- .../cards/budget/SettlementCard.test.tsx | 24 +++-------- .../Assistant/cards/budget/budgetModel.ts | 3 +- .../cards/journey/JourneyEntriesCard.test.tsx | 2 +- .../cards/packing/TodoListCard.test.tsx | 2 +- .../Assistant/cards/packing/checklistModel.ts | 6 +-- .../cards/roadtrip/RoadtripBlock.test.tsx | 6 ++- .../cards/roadtrip/RoadtripDrive.tsx | 8 ++-- .../cards/roadtrip/roadtripCardModel.test.ts | 3 +- .../cards/roadtrip/roadtripCardModel.ts | 4 +- .../Assistant/cards/tour/TourRouteDay.tsx | 5 ++- .../Assistant/cards/tour/TourRouteHead.tsx | 9 ++-- .../Assistant/cards/tour/TourRouteStops.tsx | 9 +++- .../Assistant/cards/tour/tourRouteDayModel.ts | 20 ++++++--- .../Assistant/cards/tour/tourTakeModel.ts | 3 +- .../Assistant/cards/tour/useTourRouteCard.ts | 35 +++++++++------ .../Assistant/cards/vacay/VacayBalance.tsx | 8 ++-- .../cards/vacay/vacayCardModel.test.ts | 4 +- .../Assistant/cards/vacay/vacayCardModel.ts | 4 +- .../components/Assistant/map/RoutePeek.tsx | 4 +- .../Assistant/map/routePeekModel.ts | 20 ++++++--- .../Assistant/useFoundPlacePhoto.ts | 6 +-- .../Map/AssistantGhostLineLayer.test.tsx | 5 ++- .../deepLinks/useTourDeepLink.test.ts | 12 ++++-- .../tripPlanner/useAssistantRouteLines.ts | 26 ++++------- server/scripts/eslint-baseline.json | 4 +- .../TrekPhotoSignals.repository.ts | 43 ++++++++++--------- .../collections/collections-items.service.ts | 36 ++++++++-------- .../nest/common/validate-managed-routes.ts | 1 + server/tests/unit/mcp/tools-journey.test.ts | 9 ++-- .../assistant-proposals.service.test.ts | 1 - .../assistant/assistant-tools.service.test.ts | 3 +- .../cards/packing/todo-cards.test.ts | 3 +- .../cards/roadtrip/roadtrip-changes.test.ts | 4 +- .../cards/roadtrip/roadtrip-drive.test.ts | 3 +- .../assistant/cards/vacay/vacay-cards.test.ts | 4 +- .../unit/nest/journey-book-photos.mcp.test.ts | 5 ++- .../tests/unit/nest/journey-book.mcp.test.ts | 16 ++++--- .../unit/nest/photo-resolver.service.test.ts | 5 +-- .../nest/photos/photo-signals.service.test.ts | 23 +++++----- shared/scripts/i18n-parity.mjs | 1 + shared/src/assistant/cards/card-parts.ts | 5 ++- .../assistant/cards/tour-route.card.spec.ts | 5 +-- shared/src/assistant/cards/tour-route.card.ts | 12 ++++-- shared/src/book/photo-pick.spec.ts | 11 +++-- shared/src/book/photo-pick.ts | 7 ++- shared/src/i18n/ar/assistantVacay.ts | 2 +- shared/src/i18n/az/assistantVacay.ts | 2 +- shared/src/i18n/br/assistantVacay.ts | 2 +- shared/src/i18n/ca/assistantVacay.ts | 2 +- shared/src/i18n/cs/assistantVacay.ts | 2 +- shared/src/i18n/de/assistantVacay.ts | 2 +- shared/src/i18n/en/assistantVacay.ts | 2 +- shared/src/i18n/es/assistantVacay.ts | 2 +- shared/src/i18n/et/assistantVacay.ts | 2 +- shared/src/i18n/fr/assistantVacay.ts | 2 +- shared/src/i18n/gr/assistantVacay.ts | 2 +- shared/src/i18n/hu/assistantVacay.ts | 2 +- shared/src/i18n/id/assistantVacay.ts | 2 +- shared/src/i18n/it/assistantVacay.ts | 2 +- shared/src/i18n/ja/assistantTour.ts | 6 ++- shared/src/i18n/ja/assistantVacay.ts | 2 +- shared/src/i18n/ko/assistantVacay.ts | 2 +- shared/src/i18n/nl/assistantVacay.ts | 2 +- shared/src/i18n/pl/assistantVacay.ts | 2 +- shared/src/i18n/ru/assistantVacay.ts | 2 +- shared/src/i18n/sk/assistantVacay.ts | 2 +- shared/src/i18n/sv/assistantVacay.ts | 2 +- shared/src/i18n/th/assistantVacay.ts | 2 +- shared/src/i18n/tr/assistantVacay.ts | 2 +- shared/src/i18n/uk/assistantVacay.ts | 2 +- shared/src/i18n/vi/assistantVacay.ts | 2 +- shared/src/i18n/zh-TW/assistantVacay.ts | 2 +- shared/src/i18n/zh/assistantVacay.ts | 2 +- shared/src/roadtrip/charging.schema.ts | 5 ++- 77 files changed, 293 insertions(+), 244 deletions(-) diff --git a/client/src/components/Assistant/cards/EntityActions.test.tsx b/client/src/components/Assistant/cards/EntityActions.test.tsx index 417d04761..785d73d41 100644 --- a/client/src/components/Assistant/cards/EntityActions.test.tsx +++ b/client/src/components/Assistant/cards/EntityActions.test.tsx @@ -28,7 +28,11 @@ vi.mock('./cardLooks', async (importOriginal) => { params: { tour: String(applied.ids[0]) }, }), }, - budget: { ...actual.ENTITY_LOOK.budget, open: () => ({ kind: 'tab', tab: 'finanzplan' }) }, + budget: { + ...actual.ENTITY_LOOK.budget, + openLabelKey: undefined, + open: () => ({ kind: 'tab', tab: 'finanzplan' }), + }, reservation: { ...actual.ENTITY_LOOK.reservation, openLabelKey: undefined, diff --git a/client/src/components/Assistant/cards/budget/ExpensesCard.test.tsx b/client/src/components/Assistant/cards/budget/ExpensesCard.test.tsx index ed59c79f0..6b72663ee 100644 --- a/client/src/components/Assistant/cards/budget/ExpensesCard.test.tsx +++ b/client/src/components/Assistant/cards/budget/ExpensesCard.test.tsx @@ -8,7 +8,7 @@ * the one it replaces, the facts an update changes, the whole split with * paid and highlighted parts, what a run of new expenses adds up to). */ -import type { AssistantMessage, TrekCard } from '@trek/shared'; +import type { TrekCard } from '@trek/shared'; import type { ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -22,21 +22,17 @@ import { ExpenseAside, ExpenseBody, ExpenseFacts, ExpensesTogether } from './Exp import ExpensesCard from './ExpensesCard'; const AUTH = useAuthStore.getState(); -const MESSAGE: AssistantMessage = { - id: 'm1', - role: 'assistant', - status: 'complete', - createdAt: '2026-10-22T08:00:00.000Z', - parts: [], -}; -const money = (amount: number, currency = 'EUR') => formatMoney(amount, currency, 'en'); +// Intl puts a no-break space before the symbol, and text and label queries read it as a plain one. +const money = (amount: number, currency = 'EUR') => formatMoney(amount, currency, 'en').replace(/[\u00a0\u202f]/g, ' '); +// An accessible name is matched as it stands, so it keeps the no-break space. +const moneyName = (amount: number) => formatMoney(amount, 'EUR', 'en'); function inView(ui: ReactNode, view: Partial = {}) { const value: AssistantView = { tripId: 4, variant: 'window', ...view }; return render({ui}); } -const answer = (card: TrekCard) => ; +const answer = (card: TrekCard) => ; beforeEach(() => { useAuthStore.setState({ user: buildUser({ id: ANA.id }) }); @@ -106,7 +102,7 @@ describe('the expense list', () => { inView(answer(expenses([expense({ id: null, name: 'Gelato' })])), { onOpen: vi.fn() }); expect(screen.getByText('Gelato')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Gelato/ })).toBeNull(); - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); }); @@ -162,14 +158,14 @@ describe('an expense in a change row', () => { expect(screen.getByLabelText(`You: ${money(28)}`)).toBeInTheDocument(); expect(screen.getByLabelText(`Ben: ${money(28)}, Paid`).firstElementChild).toHaveClass('text-success'); // The one the change is about stands out, and says whether the tick is set or taken off. - const cleo = screen.getByRole('img', { name: `Cleo: ${money(28)}, Not paid` }); + const cleo = screen.getByRole('img', { name: `Cleo: ${moneyName(28)}, Not paid` }); expect(cleo.firstElementChild).toHaveClass('bg-accent'); expect(cleo.querySelector('.lucide-check')).toBeNull(); first.unmount(); const ticked = expense({ shares: split.shares.map((share) => ({ ...share, paid: true })), highlight: [CLEO.id] }); const tick = inView(); - const paid = screen.getByRole('img', { name: `Cleo: ${money(28)}, Paid` }); + const paid = screen.getByRole('img', { name: `Cleo: ${moneyName(28)}, Paid` }); expect(paid.firstElementChild).toHaveClass('bg-accent'); expect(paid.querySelector('.lucide-check')).not.toBeNull(); tick.unmount(); @@ -185,7 +181,7 @@ describe('an expense in a change row', () => { const about = expense({ shares: crowd, highlight: [crowd[2]?.person.id ?? 0] }); const third = inView(); expect(screen.queryByText(`${money(14)} per person`)).toBeNull(); - const standsOut = screen.getByRole('img', { name: `Cleo: ${money(14)}, Not paid` }); + const standsOut = screen.getByRole('img', { name: `Cleo: ${moneyName(14)}, Not paid` }); expect(standsOut.firstElementChild).toHaveClass('bg-accent'); third.unmount(); diff --git a/client/src/components/Assistant/cards/budget/PaymentsCard.test.tsx b/client/src/components/Assistant/cards/budget/PaymentsCard.test.tsx index 46c7385a0..9acffda77 100644 --- a/client/src/components/Assistant/cards/budget/PaymentsCard.test.tsx +++ b/client/src/components/Assistant/cards/budget/PaymentsCard.test.tsx @@ -6,7 +6,6 @@ * change row (the amount it replaces struck through, the people it no longer * moves money between, the two faces under the row). */ -import type { AssistantMessage } from '@trek/shared'; import type { ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -19,14 +18,8 @@ import { type AssistantView, AssistantViewContext } from '../../assistantViewCon import PaymentsCard, { PaymentAside, PaymentBody, PaymentChangeFacts } from './PaymentsCard'; const AUTH = useAuthStore.getState(); -const MESSAGE: AssistantMessage = { - id: 'm1', - role: 'assistant', - status: 'complete', - createdAt: '2026-10-22T08:00:00.000Z', - parts: [], -}; -const money = (amount: number, currency = 'EUR') => formatMoney(amount, currency, 'en'); +// Intl puts a no-break space before the symbol, and text and label queries read it as a plain one. +const money = (amount: number, currency = 'EUR') => formatMoney(amount, currency, 'en').replace(/[\u00a0\u202f]/g, ' '); function inView(ui: ReactNode, view: Partial = {}) { const value: AssistantView = { tripId: 4, variant: 'window', ...view }; @@ -45,7 +38,7 @@ describe('payments', () => { it('FE-AI-CARD-BUD-015: the history names who paid whom, how much, when and why, and opens the Costs tab', () => { const onOpen = vi.fn(); const card = payments([payment({ note: 'Bank transfer' }), payment({ id: 6, from: CLEO, to: BEN, date: null })], 2); - inView(, { onOpen }); + inView(, { onOpen }); expect(screen.getByRole('region', { name: 'Settle history' })).toBeInTheDocument(); expect(screen.getByText('4')).toBeInTheDocument(); expect(screen.getAllByText(money(42.5))).toHaveLength(2); diff --git a/client/src/components/Assistant/cards/budget/SettlementCard.test.tsx b/client/src/components/Assistant/cards/budget/SettlementCard.test.tsx index 91aa7131f..700fdf8c5 100644 --- a/client/src/components/Assistant/cards/budget/SettlementCard.test.tsx +++ b/client/src/components/Assistant/cards/budget/SettlementCard.test.tsx @@ -6,11 +6,7 @@ * middle), the entries no rate converts, the payments already recorded, and * each traveller's share with how much of it is marked paid. */ -import type { - AssistantMessage, - CostPerPersonCard as CostPerPersonCardData, - SettlementCard as SettlementData, -} from '@trek/shared'; +import type { CostPerPersonCard as CostPerPersonCardData, SettlementCard as SettlementData } from '@trek/shared'; import type { ReactNode } from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -24,14 +20,8 @@ import CostPerPersonCard from './CostPerPersonCard'; import SettlementCard from './SettlementCard'; const AUTH = useAuthStore.getState(); -const MESSAGE: AssistantMessage = { - id: 'm1', - role: 'assistant', - status: 'complete', - createdAt: '2026-10-22T08:00:00.000Z', - parts: [], -}; -const money = (amount: number) => formatMoney(amount, 'EUR', 'en'); +// Intl puts a no-break space before the symbol, and text and label queries read it as a plain one. +const money = (amount: number) => formatMoney(amount, 'EUR', 'en').replace(/[\u00a0\u202f]/g, ' '); function inView(ui: ReactNode, view: Partial = {}) { const value: AssistantView = { tripId: 4, variant: 'window', ...view }; @@ -70,7 +60,7 @@ afterEach(() => { describe('settle up', () => { it('FE-AI-CARD-BUD-018: names who pays whom, the balances on their bars and the payments already recorded', () => { const onOpen = vi.fn(); - inView(, { onOpen }); + inView(, { onOpen }); expect(screen.getByRole('region', { name: 'Settle up' })).toBeInTheDocument(); expect(screen.getByText(money(42.5))).toBeInTheDocument(); expect(screen.getByText(money(18))).toBeInTheDocument(); @@ -90,7 +80,7 @@ describe('settle up', () => { recorded: 0, unconverted: { entries: 0, currencies: [] }, }); - inView(); + inView(); expect(screen.getByText("Everyone's square")).toBeInTheDocument(); expect(screen.getByText('No payments outstanding right now.')).toBeInTheDocument(); expect(screen.getByText(money(0))).toHaveClass('text-content-faint'); @@ -100,7 +90,7 @@ describe('settle up', () => { it('FE-AI-CARD-BUD-020: a long settle up counts the payments and the balances it leaves out', () => { const card = settle({ moreFlows: 3, moreBalances: 7 }); - inView(); + inView(); expect(screen.getByText('3 more')).toBeInTheDocument(); expect(screen.getByText('5')).toBeInTheDocument(); expect(screen.getByText('7 more')).toBeInTheDocument(); @@ -118,7 +108,7 @@ describe('the share per traveller', () => { ], more: 1, }; - inView(, { onOpen: vi.fn() }); + inView(, { onOpen: vi.fn() }); expect(screen.getByRole('region', { name: 'Per Person' })).toBeInTheDocument(); expect(screen.getByText(money(69))).toBeInTheDocument(); expect(screen.getByText('2 expenses')).toBeInTheDocument(); diff --git a/client/src/components/Assistant/cards/budget/budgetModel.ts b/client/src/components/Assistant/cards/budget/budgetModel.ts index 44bf3e430..d0c0a1352 100644 --- a/client/src/components/Assistant/cards/budget/budgetModel.ts +++ b/client/src/components/Assistant/cards/budget/budgetModel.ts @@ -80,7 +80,8 @@ export function mergePayments(entries: readonly CardEntry[]): PaymentsCard | nul export function latestOf(entries: readonly CardEntry[], kind: 'settlement'): SettlementCard | null; export function latestOf(entries: readonly CardEntry[], kind: 'costPerPerson'): CostPerPersonCard | null; export function latestOf(entries: readonly CardEntry[], kind: 'settlement' | 'costPerPerson') { - return cardsOf(entries, kind).at(-1) ?? null; + const cards = cardsOf(entries, kind); + return cards[cards.length - 1] ?? null; } /** How many people of a split get a badge of their own before an even one reads as a stack. */ diff --git a/client/src/components/Assistant/cards/journey/JourneyEntriesCard.test.tsx b/client/src/components/Assistant/cards/journey/JourneyEntriesCard.test.tsx index e06013017..142c03ce2 100644 --- a/client/src/components/Assistant/cards/journey/JourneyEntriesCard.test.tsx +++ b/client/src/components/Assistant/cards/journey/JourneyEntriesCard.test.tsx @@ -102,7 +102,7 @@ describe('the journal entries card', () => { it('FE-AI-JOURNEY-002: an entry without a photo takes the width, and a known mood and weather are named', () => { const { container } = block([card([look({ photoId: null, mood: 'amazing', weather: 'cold' })])]); - expect(container.querySelector('img')).toBeNull(); + expect(container.querySelector('img[src^="/api/photos/"]')).toBeNull(); expect(screen.getByText('Amazing')).toBeInTheDocument(); // `cold` is the id the journal stores for snow. expect(screen.getByText('Snowy')).toBeInTheDocument(); diff --git a/client/src/components/Assistant/cards/packing/TodoListCard.test.tsx b/client/src/components/Assistant/cards/packing/TodoListCard.test.tsx index 48b9e7935..e6313b076 100644 --- a/client/src/components/Assistant/cards/packing/TodoListCard.test.tsx +++ b/client/src/components/Assistant/cards/packing/TodoListCard.test.tsx @@ -103,7 +103,7 @@ describe('TodoListCard', () => { }); it("FE-AI-TODO-002: priorities take the panel tones; a passed due date turns red by the reader's own day", () => { - inView(); + inView(, { onOpen: vi.fn() }); expect(screen.getByText('P1').closest('span')).toHaveClass('text-danger'); expect(screen.getByText('P2').closest('span')).toHaveClass('text-warning'); const due = screen.getAllByRole('button', { name: /Show this day in the plan/ }); diff --git a/client/src/components/Assistant/cards/packing/checklistModel.ts b/client/src/components/Assistant/cards/packing/checklistModel.ts index 1a777639c..ba4b10bdf 100644 --- a/client/src/components/Assistant/cards/packing/checklistModel.ts +++ b/client/src/components/Assistant/cards/packing/checklistModel.ts @@ -63,9 +63,9 @@ function packingGroupKey(group: PackingCardGroup): string { } /** Rows joined by id, a later card's row replacing an earlier one; rows without an id are all kept. */ -function joinRows(earlier: readonly T[], later: readonly T[]): T[] { - const ids = new Set(later.flatMap((row) => (row.id === null ? [] : [row.id]))); - return [...earlier.filter((row) => row.id === null || !ids.has(row.id)), ...later]; +function joinRows(earlier: readonly T[], later: readonly T[]): T[] { + const ids = new Set(later.flatMap((row) => (typeof row.id === 'number' ? [row.id] : []))); + return [...earlier.filter((row) => typeof row.id !== 'number' || !ids.has(row.id)), ...later]; } /** Whether a card shows every row it counts, so its figures can be counted again from its rows. */ diff --git a/client/src/components/Assistant/cards/roadtrip/RoadtripBlock.test.tsx b/client/src/components/Assistant/cards/roadtrip/RoadtripBlock.test.tsx index cf1d9d48a..881dd9848 100644 --- a/client/src/components/Assistant/cards/roadtrip/RoadtripBlock.test.tsx +++ b/client/src/components/Assistant/cards/roadtrip/RoadtripBlock.test.tsx @@ -13,7 +13,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { charger, DRIVE, FLORENCE_DAY, MUNICH_DAY } from '../../../../../tests/helpers/assistantRoadtrip'; import { fireEvent, render, screen } from '../../../../../tests/helpers/render'; -import { useAssistantStore } from '../../../../store/assistantStore'; +import { chatOf, useAssistantStore } from '../../../../store/assistantStore'; import { type AssistantView, AssistantViewContext } from '../../assistantViewContext'; import RoadtripBlock from './RoadtripBlock'; @@ -122,7 +122,9 @@ describe('RoadtripBlock', () => { }); it('FE-AI-RT-016: opens on the day the chat was opened on and says where the battery runs dry', () => { - useAssistantStore.setState({ focus: { dayId: FLORENCE_DAY.dayId } }); + useAssistantStore.setState((s) => ({ + byTrip: { ...s.byTrip, 4: { ...chatOf(s, 4), focus: { dayId: FLORENCE_DAY.dayId } } }, + })); const dry = { ...DRIVE, days: [MUNICH_DAY, { ...FLORENCE_DAY, dryKm: 280 }] }; renderBlock([{ toolCallId: 'call_1', card: dry }]); expect(screen.getByText('Florence')).toBeInTheDocument(); diff --git a/client/src/components/Assistant/cards/roadtrip/RoadtripDrive.tsx b/client/src/components/Assistant/cards/roadtrip/RoadtripDrive.tsx index e4cc84247..1d60d2122 100644 --- a/client/src/components/Assistant/cards/roadtrip/RoadtripDrive.tsx +++ b/client/src/components/Assistant/cards/roadtrip/RoadtripDrive.tsx @@ -3,7 +3,7 @@ import { CalendarDays, CarFront, Crosshair, MapPin, Route, TriangleAlert, Zap } import { type ReactNode, useMemo, useState } from 'react'; import { useTranslation } from '../../../../i18n'; -import { useAssistantStore } from '../../../../store/assistantStore'; +import { chatOf, useAssistantStore } from '../../../../store/assistantStore'; import { SoftPill } from '../../../Planner/planParts'; import { dayColor } from '../../../Roadtrip/dayColors'; import { formatDurationShort } from '../../../Roadtrip/roadtripModel'; @@ -60,11 +60,13 @@ export default function RoadtripDrive({ variants?: ReactNode; }) { const { t } = useTranslation(); - const { variant } = useAssistantView(); + const { tripId, variant } = useAssistantView(); const format = useCardFormat(); const map = useRoadtripCardMap(); // The day the chat was opened on, read once: the card opens there, and on the first driving day otherwise. - const [selected, setSelected] = useState(() => defaultDayIndex(card, useAssistantStore.getState().focus?.dayId)); + const [selected, setSelected] = useState(() => + defaultDayIndex(card, chatOf(useAssistantStore.getState(), tripId).focus?.dayId) + ); const tiles = useMemo(() => card.days.map(toRoadtripDay), [card]); const day = card.days[selected] ?? card.days[0]; const railDay = tiles[selected] ?? tiles[0]; diff --git a/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.test.ts b/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.test.ts index a0badbe6d..dfbeb56a4 100644 --- a/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.test.ts +++ b/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.test.ts @@ -12,6 +12,7 @@ import { describe, expect, it } from 'vitest'; import { charger, DRIVE, FLORENCE_DAY, MUNICH_DAY } from '../../../../../tests/helpers/assistantRoadtrip'; import { dayColor } from '../../../Roadtrip/dayColors'; import { CARD_RULES } from '../cardLooks'; +import type { EntityLook } from '../cardTypes'; import { chargersOnDay, dayHasWarning, @@ -143,7 +144,7 @@ describe('roadtripCardModel', () => { }); it('FE-AI-RT-007: an applied road trip change opens its day in the road trip and shows the day on the map', () => { - const look = ROADTRIP_LOOK.entities.roadtrip; + const look: EntityLook = ROADTRIP_LOOK.entities.roadtrip; const applied = { toolCallId: 'call_1', entity: 'roadtrip' as const, ids: [], dayId: 31, undoUntil: null }; expect(look.onMap?.(applied, null)).toEqual({ placeId: null, dayId: 31 }); expect(look.open?.(applied, null)).toEqual({ kind: 'tab', tab: 'roadtrip', params: { day: '31' } }); diff --git a/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.ts b/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.ts index 58bcb2ee7..08b9fc959 100644 --- a/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.ts +++ b/client/src/components/Assistant/cards/roadtrip/roadtripCardModel.ts @@ -50,7 +50,9 @@ function stopOf(stop: RoadtripCardStop, index: number, dayId: number): RoadtripS stopType: stop.stopType, fillPercent: stop.fillPercent, offRoadMeters: stop.offRoadM, - ...(stop.carrier ? { carrier: stop.carrier } : {}), + ...(stop.carrier + ? { carrier: { ...stop.carrier, code: stop.carrier.code ?? null, at: stop.carrier.at ?? null } } + : {}), ...(stop.autoNight ? { automaticNight: stop.autoNight } : {}), ...(stop.bookend ? { diff --git a/client/src/components/Assistant/cards/tour/TourRouteDay.tsx b/client/src/components/Assistant/cards/tour/TourRouteDay.tsx index 8622fdd0b..c02860092 100644 --- a/client/src/components/Assistant/cards/tour/TourRouteDay.tsx +++ b/client/src/components/Assistant/cards/tour/TourRouteDay.tsx @@ -1,5 +1,6 @@ import type { TourRouteCard, TourRouteSchedule } from '@trek/shared'; import { Clock, Droplet, Sunrise, Sunset } from 'lucide-react'; +import { createElement } from 'react'; import { useTranslation } from '../../../../i18n'; import { SoftPill } from '../../../Planner/planParts'; @@ -29,12 +30,12 @@ function Span({ span, className }: { span: BarSpan; className: string }) { /** One hour of the forecast: the hour, the sky, the temperature, and the rain when it is likely. */ function HourCell({ hour }: { hour: TourRouteSchedule['hours'][number] }) { - const Icon = weatherIconFor(hour.main); + const icon = weatherIconFor(hour.main); const rain = hour.rainPct !== null && hour.rainPct >= RAIN_SHOWN_PCT ? hour.rainPct : null; return (
  • {String(hour.hour).padStart(2, '0')} -