import React, { useEffect, useId, useMemo, useState } from 'react' import { Search, Bookmark, ArrowRight, Loader2 } from 'lucide-react' import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' import { INPUT } from '../shared/dialogParts' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { collectionsApi } from '../../api/collections' import { getApiErrorMessage } from '../../utils/apiError' import type { Collection } from '@trek/shared' interface SaveTripPlacesToListModalProps { isOpen: boolean tripId: number /** The selected trip place ids to copy into the chosen list. */ placeIds: number[] onClose: () => void /** Called after a successful save (e.g. to clear the trip selection). */ onDone: () => void } /** * Bulk "save to collection" for the trip place list: pick one of the user's lists * and copy every selected trip place into it at once (server dedups by name/coords). */ export default function SaveTripPlacesToListModal({ isOpen, tripId, placeIds, onClose, onDone }: SaveTripPlacesToListModalProps): React.ReactElement | null { const { t } = useTranslation() const toast = useToast() const labelId = useId() const [lists, setLists] = useState([]) const [loading, setLoading] = useState(false) const [search, setSearch] = useState('') const [busyId, setBusyId] = useState(null) useEffect(() => { if (!isOpen) return let cancelled = false setLoading(true) setSearch('') collectionsApi.list() // Only lists the user can add to (their own or an editor/admin share). The // server still enforces this; here we drop lists that are clearly read-only. .then(res => { if (!cancelled) setLists((res.collections ?? []).filter(c => c.is_owner !== false)) }) .catch(() => { if (!cancelled) setLists([]) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [isOpen]) const filtered = useMemo(() => { const q = search.trim().toLowerCase() return q ? lists.filter(l => l.name.toLowerCase().includes(q)) : lists }, [lists, search]) if (!isOpen) return null const pick = async (list: Collection) => { if (busyId != null || placeIds.length === 0) return setBusyId(list.id) try { const res = await collectionsApi.saveFromTripMany(list.id, tripId, placeIds) if (res.copied > 0) toast.success(t('collections.addedNToList', { count: res.copied, name: list.name })) if (res.skipped.length > 0) toast.info(t('collections.skippedDuplicates', { count: res.skipped.length })) if (res.copied === 0 && res.skipped.length === 0) toast.info(t('collections.copyNothing')) onDone() onClose() } catch (err) { toast.error(getApiErrorMessage(err, t('common.error'))) } finally { setBusyId(null) } } return ( } tint={NEUTRAL_TINT} labelId={labelId} onClose={onClose} title={t('collections.saveNToList', { count: placeIds.length })} sub={t('collections.saveToListHint')} /> )} footer={( {t('common.cancel')} )} > {lists.length > 5 && (
)} {loading ? (
) : filtered.length === 0 ? (

{t('collections.noOwnLists')}

) : (
{filtered.map(list => { const busy = busyId === list.id return ( ) })}
)}
) }