import type { AssistantFoundPlace } from '@trek/shared'; import { Check, ChevronDown, LoaderCircle, MapPin, Plus, Star } from 'lucide-react'; import { createElement, type ReactNode, useRef } from 'react'; import { useTranslation } from '../../i18n'; import { getCategoryIcon } from '../shared/categoryIcons'; import { PopoverListPanel } from '../shared/dialogParts'; import { Tooltip } from '../shared/Tooltip'; import { usePopoverList } from '../shared/usePopoverList'; import { useAssistantView } from './assistantViewContext'; import type { useAssistantPlaceStrip } from './useAssistantPlaceStrip'; import { useFoundPlacePhoto } from './useFoundPlacePhoto'; type Strip = ReturnType; const ACTION = 'inline-flex items-center gap-1 rounded-lg bg-surface-secondary text-caption font-medium text-content transition-colors hover:bg-surface-hover disabled:cursor-default disabled:opacity-50 disabled:hover:bg-surface-secondary'; /** "+ Day 3" for the day in focus, a list of the trip's days beside it; "Pick a day" when no day is in focus. */ function SuggestButton({ place, strip, tall }: { place: AssistantFoundPlace; strip: Strip; tall: boolean }) { const { t } = useTranslation(); const { days, focusDay, disabled, blockedKey, adding } = strip; const list = usePopoverList(focusDay ? days.findIndex((d) => d.id === focusDay.id) : 0, { estimatedHeight: Math.ceil(days.length / 7) * 44 + 16, }); const { open, toggle, close, triggerRef, listId } = list; const height = tall ? 'h-10' : 'h-7'; const busy = adding === place.ref; const pick = (dayId: number) => { close(); void strip.suggest(place, dayId); }; const buttons = ( {focusDay ? ( <> ) : ( )} ); return ( <> {blockedKey ? ( {/* A disabled button gets no pointer events; the wrapper carries the reason. */} {buttons} ) : ( buttons )} {/* A grid of day tiles, not a list: a month-long trip still fits in a few rows. */}
{days.map((day) => { const current = day.id === focusDay?.id; return ( ); })}
); } type Category = ReturnType; /** The picture, or the category's icon on its colour, with the category and whether it is open on it. */ function PlaceMedia({ place, photo, category, }: { place: AssistantFoundPlace; photo: string | null; category: Category; }) { const { t } = useTranslation(); const icon = category ? getCategoryIcon(category.icon) : MapPin; return ( {photo ? ( ) : ( {createElement(icon, { size: 30, 'aria-hidden': true, style: { color: category?.color ?? 'var(--text-faint)' }, })} )} {category && photo && ( {createElement(icon, { size: 11, 'aria-hidden': true, className: 'flex-shrink-0', style: { color: category.color }, })} {category.name} )} {place.openNow !== null && ( )} ); } /** The card's one way on: "On map" for a place the trip has, "Suggested" once it is, else "+ Day 3". */ function PlaceAction({ place, strip, suggested, tall, }: { place: AssistantFoundPlace; strip: Strip; suggested: boolean; tall: boolean; }): ReactNode { const { t } = useTranslation(); if (place.inTripPlaceId !== null) { return ( ); } if (suggested) { return ( ); } return strip.canSuggest ? : null; } /** * One place a search found, as a card: its picture from TREK's photo proxy * with the category it most likely belongs in on it (or that category's icon * on its colour), its name, address and rating, whether it is open now while * the trip runs, how far it is from the last stop of the day in focus, and one * way on: "+ Day 3" for a new place, "On map" for one the trip has. A click on * the card shows it on the map, and resting the pointer on it pins it there * for a moment. */ export default function AssistantPlaceCard({ place, strip, suggested, }: { place: AssistantFoundPlace; strip: Strip; suggested: boolean; }) { const { t } = useTranslation(); const { variant, density } = useAssistantView(); const ref = useRef(null); const photo = useFoundPlacePhoto(place, ref); const category = strip.categoryOf(place); const distance = strip.distanceOf(place); const tall = variant === 'fullscreen'; return (
  • strip.hover(place)} onMouseLeave={() => strip.hover(null)} >
  • ); }