import { useEffect, useMemo, useState, useCallback } from 'react' import { useNavigate } from 'react-router' import { Bookmark, BookmarkCheck, Check, CheckCircle2, Loader2, Plus, X } from 'lucide-react' import MSheet from '../../mobile/components/MSheet' import MIconBtn from '../../mobile/components/MIconBtn' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { collectionsApi } from '../../api/collections' import { useSaveToCollectionStore } from '../../store/saveToCollectionStore' import { getApiErrorMessage } from '../../utils/apiError' import { STATUS_META, nextStatus } from '../../pages/collections/collectionsModel' import type { Collection, CollectionMembership, CollectionStatus } from '@trek/shared' /** * Mobile counterpart of SaveToCollectionModal — the same store-driven list * picker (load lists + membership, toggle the place in/out of each), dressed in * the mobile design language (MSheet card, m-* tokens) so it matches the place * detail sheet. Rendered instead of the desktop modal on phones (see App.tsx). */ export default function MSaveToCollectionSheet() { const target = useSaveToCollectionStore(s => s.target) const close = useSaveToCollectionStore(s => s.close) const bumpVersion = useSaveToCollectionStore(s => s.bumpVersion) const { t } = useTranslation() const toast = useToast() const navigate = useNavigate() const [lists, setLists] = useState([]) const [membership, setMembership] = useState(null) const [loading, setLoading] = useState(false) const [busyId, setBusyId] = useState(null) const membershipQuery = useMemo(() => { if (!target) return null return { google_place_id: target.google_place_id ?? undefined, google_ftid: target.google_ftid ?? undefined, name: target.name, lat: target.lat ?? undefined, lng: target.lng ?? undefined, } }, [target]) const refreshMembership = useCallback(async () => { if (!membershipQuery) return try { setMembership(await collectionsApi.membership(membershipQuery)) } catch { setMembership({ saved: false, lists: [] }) } }, [membershipQuery]) // Load lists + membership whenever the picker opens for a new target. useEffect(() => { if (!target) return let cancelled = false setLoading(true) setMembership(null) Promise.all([ collectionsApi.list().catch(() => ({ collections: [], incomingInvites: [] })), membershipQuery ? collectionsApi.membership(membershipQuery).catch(() => ({ saved: false, lists: [] as CollectionMembership['lists'] })) : Promise.resolve({ saved: false, lists: [] as CollectionMembership['lists'] }), ]) .then(([listRes, m]) => { if (cancelled) return setLists(listRes.collections) setMembership(m) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [target]) const savedByCollection = new Map() for (const l of membership?.lists ?? []) savedByCollection.set(l.collection_id, l) /** Lists holding this place that the viewer may edit and that are not visited yet. */ const unvisited = (membership?.lists ?? []).filter(l => l.can_edit && l.status !== 'visited') const handleStatus = async (entry: CollectionMembership['lists'][number], next: CollectionStatus) => { if (busyId != null) return setBusyId(entry.collection_id) try { await collectionsApi.setStatus(entry.place_id, next) await refreshMembership() bumpVersion() } catch (err) { toast.error(getApiErrorMessage(err, t('common.error'))) } finally { setBusyId(null) } } const handleVisitedEverywhere = async () => { if (busyId != null || unvisited.length === 0) return setBusyId(-1) try { const { updated } = await collectionsApi.setStatusMany(unvisited.map(l => l.place_id), 'visited') toast.success(t('collections.markedVisited', { count: updated })) await refreshMembership() bumpVersion() } catch (err) { toast.error(getApiErrorMessage(err, t('common.error'))) } finally { setBusyId(null) } } const handleToggle = async (list: Collection) => { if (busyId != null || !target) return const savedPlaceId = savedByCollection.get(list.id)?.place_id setBusyId(list.id) try { if (savedPlaceId != null) { await collectionsApi.deletePlace(savedPlaceId) toast.success(t('collections.removedFromList', { name: list.name })) } else { await collectionsApi.savePlace({ collection_id: list.id, source_trip_id: target.source_trip_id ?? null, source_place_id: target.source_place_id ?? null, name: target.name, description: target.description ?? null, lat: target.lat ?? null, lng: target.lng ?? null, address: target.address ?? null, category_id: target.category_id ?? null, price: target.price ?? null, currency: target.currency ?? null, notes: target.notes ?? null, image_url: target.image_url ?? null, google_place_id: target.google_place_id ?? null, google_ftid: target.google_ftid ?? null, osm_id: target.osm_id ?? null, website: target.website ?? null, phone: target.phone ?? null, force: true, }) toast.success(t('collections.addedToList', { name: list.name })) } await refreshMembership() bumpVersion() } catch (err) { toast.error(getApiErrorMessage(err, t('common.error'))) } finally { setBusyId(null) } } return ( {/* Header — mirrors the place detail sheet: icon + title + target name + close */}
{t('collections.pickList')}
{target?.name && (
{target.name}
)}
{loading ? (
) : lists.length === 0 ? (

{t('collections.noListsYet')}

) : ( lists.map(list => { const entry = savedByCollection.get(list.id) const saved = !!entry const busy = busyId === list.id const statusMeta = entry ? STATUS_META[entry.status] : null const StatusIcon = statusMeta?.icon return ( ) }) )}
{unvisited.length > 0 && (
)} {lists.length > 0 && (
)}
) }