mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
feat(assistant): booking and stay cards, and a read of the bookings with their details
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import type { Reservation } from '@trek/shared';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useBookingLink } from './useBookingLink';
|
||||
|
||||
/**
|
||||
* FE-HOOK-BOOKLINK-001 to FE-HOOK-BOOKLINK-004
|
||||
*
|
||||
* A booking the chat points at: taken once the trip's bookings are there,
|
||||
* then dropped from the URL, an unknown one dropped without a word.
|
||||
*/
|
||||
|
||||
const booking = (id: number, tripId = 5) => ({ id, trip_id: tripId, type: 'flight' }) as Reservation;
|
||||
|
||||
function setup(query: string, reservations: Reservation[], loaded: boolean) {
|
||||
const searchParams = new URLSearchParams(query);
|
||||
const setSearchParams = vi.fn((update: (params: URLSearchParams) => URLSearchParams) => update(searchParams));
|
||||
const take = vi.fn();
|
||||
const hook = renderHook(
|
||||
({ list, done }) =>
|
||||
useBookingLink({
|
||||
searchParams,
|
||||
setSearchParams: setSearchParams as never,
|
||||
tripId: 5,
|
||||
reservations: list,
|
||||
loaded: done,
|
||||
take,
|
||||
}),
|
||||
{ initialProps: { list: reservations, done: loaded } }
|
||||
);
|
||||
return { searchParams, setSearchParams, take, hook };
|
||||
}
|
||||
|
||||
describe('useBookingLink', () => {
|
||||
it('FE-HOOK-BOOKLINK-001: opens a booking of this trip once and drops the link', () => {
|
||||
const { searchParams, take, hook } = setup('tab=transports&booking=7', [booking(7)], false);
|
||||
expect(take).toHaveBeenCalledTimes(1);
|
||||
expect(take).toHaveBeenCalledWith(booking(7), false);
|
||||
expect(searchParams.has('booking')).toBe(false);
|
||||
expect(searchParams.get('tab')).toBe('transports');
|
||||
hook.rerender({ list: [booking(7)], done: true });
|
||||
expect(take).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('FE-HOOK-BOOKLINK-002: hands on map=1 and drops it with the link', () => {
|
||||
const { searchParams, take } = setup('booking=7&map=1', [booking(7)], true);
|
||||
expect(take).toHaveBeenCalledWith(booking(7), true);
|
||||
expect(searchParams.has('map')).toBe(false);
|
||||
});
|
||||
|
||||
it("FE-HOOK-BOOKLINK-003: waits for the trip's bookings, then drops an id it does not hold", () => {
|
||||
const { searchParams, take, hook } = setup('booking=9', [booking(9, 4)], false);
|
||||
expect(searchParams.get('booking')).toBe('9');
|
||||
hook.rerender({ list: [booking(7)], done: true });
|
||||
expect(take).not.toHaveBeenCalled();
|
||||
expect(searchParams.has('booking')).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-HOOK-BOOKLINK-004: does nothing without a link', () => {
|
||||
const { setSearchParams, take } = setup('tab=plan', [booking(7)], true);
|
||||
expect(setSearchParams).not.toHaveBeenCalled();
|
||||
expect(take).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Reservation } from '@trek/shared';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { SetURLSearchParams } from 'react-router';
|
||||
|
||||
interface BookingLinkOptions {
|
||||
searchParams: URLSearchParams;
|
||||
setSearchParams: SetURLSearchParams;
|
||||
tripId: number;
|
||||
reservations: readonly Reservation[];
|
||||
/** True once this trip's bookings have loaded, so an id it does not hold can be dropped. */
|
||||
loaded: boolean;
|
||||
/** Opens the booking: its detail, or with `onMap` its route on the plan. */
|
||||
take: (booking: Reservation, onMap: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A booking the chat points at (`?booking=<id>`, `&map=1` for its route on
|
||||
* the plan), for the desktop planner and the phone alike: once the trip's
|
||||
* bookings are there it is handed to `take`, then the link leaves the URL. It
|
||||
* is taken once, so a render before the URL has dropped it does not open it
|
||||
* twice; an id the trip does not hold is dropped without a word.
|
||||
*/
|
||||
export function useBookingLink({
|
||||
searchParams,
|
||||
setSearchParams,
|
||||
tripId,
|
||||
reservations,
|
||||
loaded,
|
||||
take,
|
||||
}: BookingLinkOptions): void {
|
||||
const requested = searchParams.get('booking');
|
||||
const onMap = searchParams.get('map') === '1';
|
||||
const id = requested === null ? null : Number(requested);
|
||||
const booking = id === null ? undefined : reservations.find((r) => r.id === id && r.trip_id === tripId);
|
||||
const settled = !!booking || loaded;
|
||||
const taken = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (requested === null) {
|
||||
taken.current = null;
|
||||
return;
|
||||
}
|
||||
const link = `${requested}:${onMap}`;
|
||||
if (!settled || taken.current === link) return;
|
||||
taken.current = link;
|
||||
if (booking) take(booking, onMap);
|
||||
setSearchParams(
|
||||
(params) => {
|
||||
params.delete('booking');
|
||||
params.delete('map');
|
||||
return params;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
}, [requested, settled, booking, onMap, take, setSearchParams]);
|
||||
}
|
||||
Reference in New Issue
Block a user