mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
fix(native): the widgets show the trip the dashboard features
With several trips running at once, the widgets took whichever came first on the device, so an old test trip spanning today pushed out the journey under way, and the today widget counted its days. The dashboard's choice moves to utils/heroTrip.ts (running first, earliest start, else the next to come) and the widgets ask the same function, so the home screen always shows the trip the app opens on.
This commit is contained in:
@@ -51,6 +51,15 @@ describe('pickTrip', () => {
|
||||
expect(pickTrip(trips, '2026-11-12')?.id).toBe(2);
|
||||
});
|
||||
|
||||
it('of several running trips takes the dashboard hero: the one that started first', () => {
|
||||
const trips = [
|
||||
trip(1, '2026-11-02', '2026-11-06'),
|
||||
trip(2, '2026-10-20', '2026-11-18'),
|
||||
trip(3, '2026-11-04', '2026-11-12'),
|
||||
];
|
||||
expect(pickTrip(trips, '2026-11-06')?.id).toBe(2);
|
||||
});
|
||||
|
||||
it('skips undated, archived and finished trips', () => {
|
||||
expect(
|
||||
pickTrip(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Day, Reservation, Trip } from '../types';
|
||||
import { splitReservationDateTime } from '../utils/formatters';
|
||||
import { heroTrip, sortTripsByRelevance } from '../utils/heroTrip';
|
||||
import { hasNativeFeature } from './capabilities';
|
||||
import { isNativeIos } from './platform';
|
||||
import { reportNativeFailure } from './reportFailure';
|
||||
@@ -121,17 +122,13 @@ function minutesOf(time: string | null | undefined): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* The running trip if there is one, else the next to start. Trips without dates
|
||||
* and trips already over are not candidates.
|
||||
* The trip the dashboard's hero features (utils/heroTrip.ts): the running one,
|
||||
* else the next to start, so the widgets show the trip the app opens on.
|
||||
* Archived trips are not candidates, as on the dashboard.
|
||||
*/
|
||||
export function pickTrip(trips: Trip[], today: string): Trip | null {
|
||||
const dated = trips.flatMap((t) =>
|
||||
t.start_date && t.end_date && !t.is_archived ? [{ trip: t, start: t.start_date, end: t.end_date }] : []
|
||||
);
|
||||
const running = dated.find((d) => d.start <= today && d.end >= today);
|
||||
if (running) return running.trip;
|
||||
const upcoming = dated.filter((d) => d.start > today).sort((a, b) => a.start.localeCompare(b.start));
|
||||
return upcoming[0]?.trip ?? null;
|
||||
const active = trips.filter((t) => !t.is_archived);
|
||||
return heroTrip(sortTripsByRelevance(active, today), today);
|
||||
}
|
||||
|
||||
/** The symbols of the booking kinds a day's timeline shows next to its stops. */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import type { Trip } from '../../types'
|
||||
import { localIsoDate } from '../../utils/localDate'
|
||||
import { sortTripsByRelevance } from '../../utils/heroTrip'
|
||||
|
||||
// The dashboard works with the canonical Trip shape returned by the list/get
|
||||
// endpoints (it already carries the computed day_count/place_count/is_owner/
|
||||
@@ -83,17 +84,5 @@ export function getTripStatus(trip: DashboardTrip): 'ongoing' | 'today' | 'tomor
|
||||
}
|
||||
|
||||
export function sortTrips(trips: DashboardTrip[]): DashboardTrip[] {
|
||||
const today = localIsoToday()
|
||||
const rank = (t: DashboardTrip) => {
|
||||
if (t.start_date && t.end_date && t.start_date <= today && t.end_date >= today) return 0
|
||||
if (t.start_date && t.start_date >= today) return 1
|
||||
return 2
|
||||
}
|
||||
return [...trips].sort((a, b) => {
|
||||
const ra = rank(a), rb = rank(b)
|
||||
if (ra !== rb) return ra - rb
|
||||
const ad = a.start_date || '', bd = b.start_date || ''
|
||||
if (ra <= 1) return ad.localeCompare(bd)
|
||||
return bd.localeCompare(ad)
|
||||
})
|
||||
return sortTripsByRelevance(trips, localIsoToday())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from '../../i18n'
|
||||
import { useToast } from '../../components/shared/Toast'
|
||||
import { getApiErrorMessage } from '../../types'
|
||||
import { localIsoToday } from './dashboardModel'
|
||||
import { heroTrip } from '../../utils/heroTrip'
|
||||
import { useTripSearch } from './useTripSearch'
|
||||
import type { TripCreateRequest } from '@trek/shared'
|
||||
import {
|
||||
@@ -120,9 +121,7 @@ export function useDashboard() {
|
||||
// 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)
|
||||
|| null
|
||||
const featured = heroTrip(trips, today)
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { heroTrip, sortTripsByRelevance } from './heroTrip';
|
||||
|
||||
const trip = (id: number, start_date: string | null, end_date: string | null) => ({ id, start_date, end_date });
|
||||
|
||||
describe('heroTrip', () => {
|
||||
const today = '2026-11-06';
|
||||
const trips = [
|
||||
trip(1, '2026-01-01', '2026-01-05'),
|
||||
trip(2, '2026-12-01', '2026-12-05'),
|
||||
trip(3, '2026-11-04', '2026-11-12'),
|
||||
trip(4, '2026-10-20', '2026-11-18'),
|
||||
trip(5, null, null),
|
||||
];
|
||||
|
||||
it('orders running trips first, then the ones coming up, then the rest, latest first', () => {
|
||||
expect(sortTripsByRelevance(trips, today).map((t) => t.id)).toEqual([4, 3, 2, 1, 5]);
|
||||
});
|
||||
|
||||
it('features the running trip that started first, else the next to come', () => {
|
||||
expect(heroTrip(sortTripsByRelevance(trips, today), today)?.id).toBe(4);
|
||||
expect(heroTrip(sortTripsByRelevance([trips[0], trips[1]], today), today)?.id).toBe(2);
|
||||
expect(heroTrip(sortTripsByRelevance([trips[0], trips[4]], today), today)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Which trip the dashboard features, in one place: the dashboard's hero and
|
||||
* the iOS widgets (native/widgets.ts) both ask here, so the trip on the home
|
||||
* screen is always the one the app opens on.
|
||||
*/
|
||||
interface DatedTrip {
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
}
|
||||
|
||||
/** 0 running, 1 coming up, 2 past or undated. */
|
||||
function rank(trip: DatedTrip, today: string): number {
|
||||
if (trip.start_date && trip.end_date && trip.start_date <= today && trip.end_date >= today) return 0;
|
||||
if (trip.start_date && trip.start_date >= today) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
/** Running trips first, then the ones coming up, both soonest first; the rest latest first. */
|
||||
export function sortTripsByRelevance<T extends DatedTrip>(trips: T[], today: string): T[] {
|
||||
return [...trips].sort((a, b) => {
|
||||
const ra = rank(a, today);
|
||||
const rb = rank(b, today);
|
||||
if (ra !== rb) return ra - rb;
|
||||
const ad = a.start_date || '';
|
||||
const bd = b.start_date || '';
|
||||
if (ra <= 1) return ad.localeCompare(bd);
|
||||
return bd.localeCompare(ad);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The trip the hero features on its own merit: the first running one in that
|
||||
* order, else the first coming up; null when every trip is past or undated.
|
||||
*/
|
||||
export function heroTrip<T extends DatedTrip>(sorted: T[], today: string): T | null {
|
||||
return sorted.find((t) => rank(t, today) === 0) || sorted.find((t) => rank(t, today) === 1) || null;
|
||||
}
|
||||
Reference in New Issue
Block a user