mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(planner): a day head's pill row moves into a module of its own
The stays, hire cars and plugin label under a day's name now live in DayHeadPills, so DayPlanSidebar shrinks back under its size baseline. The stay fields are read through their types instead of an any cast, and the size, eslint and theme-disable baselines are lowered to the new counts.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"src": {
|
||||
"@typescript-eslint/no-non-null-assertion": 407,
|
||||
"@typescript-eslint/no-non-null-assertion": 406,
|
||||
"@typescript-eslint/no-misused-promises": 876,
|
||||
"no-console": 88,
|
||||
"complexity": 231,
|
||||
"@typescript-eslint/no-explicit-any": 154,
|
||||
"@typescript-eslint/no-explicit-any": 149,
|
||||
"trek/no-swallowed-catch": 135,
|
||||
"max-params": 35,
|
||||
"trek/store-without-selector": 69,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"src/components/PDF/TripPDF.tsx": 1097,
|
||||
"src/components/Planner/DayDetailPanel.test.tsx": 2333,
|
||||
"src/components/Planner/DayPlanSidebar.test.tsx": 5381,
|
||||
"src/components/Planner/DayPlanSidebar.tsx": 3052,
|
||||
"src/components/Planner/DayPlanSidebar.tsx": 3001,
|
||||
"src/components/Planner/PlaceFormModal.test.tsx": 2864,
|
||||
"src/components/Planner/PlaceFormModal.tsx": 1250,
|
||||
"src/components/Planner/PlaceInspector.tsx": 1308,
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"components/Roadtrip/RoadtripCategoryPicker.tsx": 2,
|
||||
"components/Roadtrip/RoadtripCorridorPanel.tsx": 3,
|
||||
"components/Roadtrip/RoadtripSidebarStopRows.tsx": 4,
|
||||
"components/Roadtrip/RoadtripStopPopup.tsx": 1,
|
||||
"components/Roadtrip/RoadtripTrackModal.tsx": 1,
|
||||
"components/Roadtrip/StopKindPicker.tsx": 2,
|
||||
"components/Roadtrip/dayColors.ts": 8,
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Car, Hotel, Ticket } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../i18n';
|
||||
import type { Accommodation, Reservation } from '../../types';
|
||||
import { fs } from '../shared/DialogShell';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import { SoftPill } from './planParts';
|
||||
|
||||
/** The second line of a day head: its stays, its hire cars and a plugin's name for the day. */
|
||||
export function DayHeadPills({
|
||||
dayId,
|
||||
stays,
|
||||
rentals,
|
||||
pendingStays,
|
||||
reservations,
|
||||
label,
|
||||
onPlaceClick,
|
||||
onOpenBooking,
|
||||
onOpenRental,
|
||||
}: {
|
||||
dayId: number;
|
||||
stays: Accommodation[];
|
||||
rentals: Reservation[];
|
||||
pendingStays: Set<number>;
|
||||
reservations: Reservation[];
|
||||
label?: string | null;
|
||||
onPlaceClick: (placeId: number) => void;
|
||||
onOpenBooking?: (reservation: Reservation) => void;
|
||||
onOpenRental: (reservation: Reservation) => void;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex min-w-0 flex-nowrap items-center gap-1" style={{ gridColumn: '2 / 4' }}>
|
||||
{stays.map((acc) => {
|
||||
const isCheckIn = acc.start_day_id === dayId;
|
||||
const isCheckOut = acc.end_day_id === dayId;
|
||||
const accName = acc.place_name || acc.reservation_title || '';
|
||||
// The state is said in words as well as colour: check-in green,
|
||||
// check-out red, a stay in between quiet.
|
||||
const state = isCheckOut && !isCheckIn ? t('day.checkOut') : isCheckIn ? t('day.checkIn') : null;
|
||||
const iconTone = isCheckOut && !isCheckIn ? 'text-danger' : isCheckIn ? 'text-success' : 'text-content-faint';
|
||||
// Not confirmed yet: a dashed ring says so at a glance, the tooltip in words (#2281).
|
||||
const pending = pendingStays.has(acc.id);
|
||||
const named = state ? `${state}: ${accName}` : accName;
|
||||
const tip = pending ? `${named} (${t('reservations.pending')})` : named;
|
||||
// The name opens the place, as before; a stay with a booking gets a
|
||||
// second target in the same pill that opens the booking (#2363).
|
||||
const stayBooking = onOpenBooking
|
||||
? reservations.find((r) => r.accommodation_id != null && Number(r.accommodation_id) === acc.id)
|
||||
: undefined;
|
||||
return (
|
||||
<span
|
||||
key={acc.id}
|
||||
className={`inline-flex min-w-0 max-w-max flex-[1_1_0%] items-stretch overflow-hidden rounded-full ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}
|
||||
>
|
||||
<Tooltip label={tip}>
|
||||
<button
|
||||
type="button"
|
||||
data-dp="day-pill"
|
||||
data-pending={pending || undefined}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (acc.place_id) onPlaceClick(acc.place_id);
|
||||
}}
|
||||
className={`inline-flex min-w-0 items-center gap-1 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${stayBooking ? 'pe-1.5 ps-2' : 'px-2'}`}
|
||||
style={{ ...fs(10.5), cursor: acc.place_id ? 'pointer' : 'default' }}
|
||||
>
|
||||
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
|
||||
<span className="truncate">{accName}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{stayBooking && onOpenBooking && (
|
||||
<Tooltip label={t('day.openStayBooking')}>
|
||||
<button
|
||||
type="button"
|
||||
data-dp="day-pill-booking"
|
||||
aria-label={t('day.openStayBooking')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenBooking(stayBooking);
|
||||
}}
|
||||
className="flex flex-none items-center border-s border-edge-faint pe-2 ps-1.5 text-content-faint hover:text-content"
|
||||
>
|
||||
<Ticket size={11} strokeWidth={2} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{/* Active rental car badges */}
|
||||
{rentals.map((r) => (
|
||||
<button
|
||||
type="button"
|
||||
data-dp="day-pill"
|
||||
key={`rental-${r.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenRental(r);
|
||||
}}
|
||||
className="inline-flex min-w-0 max-w-max flex-[1_1_0%] items-center gap-1 rounded-full bg-surface-card px-2 py-[2px] font-geist font-normal text-content-secondary shadow-sm hover:text-content"
|
||||
style={fs(10.5)}
|
||||
>
|
||||
<Car size={11} strokeWidth={2} className="flex-none text-content-faint" />
|
||||
<span className="truncate">{r.title}</span>
|
||||
</button>
|
||||
))}
|
||||
{/* A plugin's name for this day (which leg of the trip it belongs to). */}
|
||||
{label && <SoftPill>{label}</SoftPill>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import { projectDayItinerary } from '../Map/dayTourProjection'
|
||||
import { usePluginDaySchedule, usePluginDayTints, dayTintBackground, dayTinted, PluginDayScheduleRow, formatScheduleMinutes } from '../Plugins/PluginDaySchedule'
|
||||
import { MobileAddPlaceButton } from './DayPlanSidebarMobileAddPlaceButton'
|
||||
import { DayPlanSidebarToolbar } from './DayPlanSidebarToolbar'
|
||||
import { DayHeadPills } from './DayHeadPills'
|
||||
import { DayPlanSidebarNoteModal } from './DayPlanSidebarNoteModal'
|
||||
import { DayPlanSidebarTimeConfirmModal } from './DayPlanSidebarTimeConfirmModal'
|
||||
import { DayPlanSidebarTransportDetailModal } from './DayPlanSidebarTransportDetailModal'
|
||||
@@ -1735,59 +1736,9 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
</button>
|
||||
</div>
|
||||
{hasDayPills && (
|
||||
<div className="flex min-w-0 flex-nowrap items-center gap-1" style={{ gridColumn: '2 / 4' }}>
|
||||
{dayAccs.map(acc => {
|
||||
const isCheckIn = acc.start_day_id === day.id
|
||||
const isCheckOut = acc.end_day_id === day.id
|
||||
const accName = (acc as any).place_name || (acc as any).reservation_title
|
||||
// The state is said in words as well as colour: check-in green,
|
||||
// check-out red, a stay in between quiet.
|
||||
const state = isCheckOut && !isCheckIn ? t('day.checkOut') : isCheckIn ? t('day.checkIn') : null
|
||||
const iconTone = isCheckOut && !isCheckIn ? 'text-danger' : isCheckIn ? 'text-success' : 'text-content-faint'
|
||||
// Not confirmed yet: a dashed ring says so at a glance, the tooltip in words (#2281).
|
||||
const pending = pendingStays.has(acc.id)
|
||||
const named = state ? `${state}: ${accName}` : accName
|
||||
const tip = pending ? `${named} (${t('reservations.pending')})` : named
|
||||
// The name opens the place, as before; a stay with a booking gets a
|
||||
// second target in the same pill that opens the booking (#2363).
|
||||
const stayBooking = onOpenBooking
|
||||
? reservations.find(r => r.accommodation_id != null && Number(r.accommodation_id) === acc.id)
|
||||
: undefined
|
||||
return (
|
||||
<span key={acc.id} className={`inline-flex min-w-0 max-w-max flex-[1_1_0%] items-stretch overflow-hidden rounded-full ${pending ? 'border border-dashed border-warning bg-warning-soft' : 'bg-surface-card shadow-sm'}`}>
|
||||
<Tooltip label={tip}>
|
||||
<button type="button" data-dp="day-pill" data-pending={pending || undefined}
|
||||
onClick={e => { e.stopPropagation(); if ((acc as any).place_id) onPlaceClick((acc as any).place_id) }}
|
||||
className={`inline-flex min-w-0 items-center gap-1 py-[2px] font-geist font-normal text-content-secondary hover:text-content ${stayBooking ? 'ps-2 pe-1.5' : 'px-2'}`}
|
||||
style={{ ...fs(10.5), cursor: (acc as any).place_id ? 'pointer' : 'default' }}>
|
||||
<Hotel size={11} strokeWidth={2} className={`flex-none ${iconTone}`} />
|
||||
<span className="truncate">{accName}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{stayBooking && (
|
||||
<Tooltip label={t('day.openStayBooking')}>
|
||||
<button type="button" data-dp="day-pill-booking" aria-label={t('day.openStayBooking')}
|
||||
onClick={e => { e.stopPropagation(); onOpenBooking!(stayBooking) }}
|
||||
className="flex flex-none items-center border-s border-edge-faint ps-1.5 pe-2 text-content-faint hover:text-content">
|
||||
<Ticket size={11} strokeWidth={2} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{/* Active rental car badges */}
|
||||
{activeRentals.map(r => (
|
||||
<button type="button" data-dp="day-pill" key={`rental-${r.id}`} onClick={e => { e.stopPropagation(); if (onOpenBooking) onOpenBooking(r); else setTransportDetail(r) }}
|
||||
className="inline-flex min-w-0 max-w-max flex-[1_1_0%] items-center gap-1 rounded-full bg-surface-card px-2 py-[2px] font-geist font-normal text-content-secondary shadow-sm hover:text-content"
|
||||
style={fs(10.5)}>
|
||||
<Car size={11} strokeWidth={2} className="flex-none text-content-faint" />
|
||||
<span className="truncate">{r.title}</span>
|
||||
</button>
|
||||
))}
|
||||
{/* A plugin's name for this day (which leg of the trip it belongs to). */}
|
||||
{dayTint?.label && <SoftPill>{dayTint.label}</SoftPill>}
|
||||
</div>
|
||||
<DayHeadPills dayId={day.id} stays={dayAccs} rentals={activeRentals} pendingStays={pendingStays}
|
||||
reservations={reservations} label={dayTint?.label} onPlaceClick={onPlaceClick} onOpenBooking={onOpenBooking}
|
||||
onOpenRental={r => { if (onOpenBooking) onOpenBooking(r); else setTransportDetail(r) }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user