From 6be8a67c8261cd7125831fef4a6592b94e73ebe5 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 1 Jul 2026 15:07:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(collections):=20select=20toolbar=20?= =?UTF-8?q?=E2=80=94=20select-all,=20move/duplicate=20to=20another=20list?= =?UTF-8?q?=20(#1081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The select toggle now sits at the right of the filter row (same height as the status/category dropdowns) instead of the top toolbar. - Select mode gains a "select all / deselect all" toggle and shows even with nothing selected yet. - Selected places can be moved or duplicated into another of your lists via a target-list picker (move re-points collection_id; duplicate re-saves the place data, carrying description / category / notes / etc.). - New strings across all 22 locales. --- .../Collections/CollectionFilterBar.tsx | 13 ++- .../Collections/MoveToListModal.tsx | 90 +++++++++++++++++++ client/src/pages/CollectionsPage.tsx | 46 ++++++---- .../src/pages/collections/useCollections.ts | 32 +++++++ client/src/store/collectionStore.ts | 47 ++++++++++ client/src/styles/collections.css | 3 + shared/src/i18n/ar/collection.ts | 11 +++ shared/src/i18n/br/collection.ts | 11 +++ shared/src/i18n/cs/collection.ts | 11 +++ shared/src/i18n/de/collection.ts | 11 +++ shared/src/i18n/en/collection.ts | 11 +++ shared/src/i18n/es/collection.ts | 11 +++ shared/src/i18n/fr/collection.ts | 11 +++ shared/src/i18n/gr/collection.ts | 11 +++ shared/src/i18n/hu/collection.ts | 11 +++ shared/src/i18n/id/collection.ts | 11 +++ shared/src/i18n/it/collection.ts | 11 +++ shared/src/i18n/ja/collection.ts | 11 +++ shared/src/i18n/ko/collection.ts | 11 +++ shared/src/i18n/nl/collection.ts | 11 +++ shared/src/i18n/pl/collection.ts | 11 +++ shared/src/i18n/ru/collection.ts | 11 +++ shared/src/i18n/sv/collection.ts | 11 +++ shared/src/i18n/tr/collection.ts | 11 +++ shared/src/i18n/uk/collection.ts | 11 +++ shared/src/i18n/vi/collection.ts | 11 +++ shared/src/i18n/zh-TW/collection.ts | 11 +++ shared/src/i18n/zh/collection.ts | 11 +++ 28 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 client/src/components/Collections/MoveToListModal.tsx diff --git a/client/src/components/Collections/CollectionFilterBar.tsx b/client/src/components/Collections/CollectionFilterBar.tsx index 01ca777ff..809cf1fa9 100644 --- a/client/src/components/Collections/CollectionFilterBar.tsx +++ b/client/src/components/Collections/CollectionFilterBar.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react' -import { ChevronDown, Check, Layers, Tag } from 'lucide-react' +import { ChevronDown, Check, Layers, Tag, CheckSquare } from 'lucide-react' import type { StatusFilter } from '../../store/collectionStore' import type { TranslationFn } from '../../types' import { getCategoryIcon } from '../shared/categoryIcons' @@ -69,6 +69,9 @@ interface CollectionFilterBarProps { categoryOptions: CategoryOption[] onStatusFilter: (f: StatusFilter) => void onCategoryFilter: (f: number | 'all') => void + showSelect: boolean + selectMode: boolean + onToggleSelect: () => void t: TranslationFn } @@ -78,7 +81,8 @@ interface CollectionFilterBarProps { * Custom compact dropdowns so they barely take any space. */ export default function CollectionFilterBar({ - statusFilter, counts, categoryFilter, categoryOptions, onStatusFilter, onCategoryFilter, t, + statusFilter, counts, categoryFilter, categoryOptions, onStatusFilter, onCategoryFilter, + showSelect, selectMode, onToggleSelect, t, }: CollectionFilterBarProps): React.ReactElement { const statusOpts: Opt[] = [ { key: 'all', label: t('common.all'), count: counts.all }, @@ -103,6 +107,11 @@ export default function CollectionFilterBar({ {categoryOptions.length > 0 && ( onCategoryFilter(k as number | 'all')} lead={} /> )} + {showSelect && ( + + )} ) } diff --git a/client/src/components/Collections/MoveToListModal.tsx b/client/src/components/Collections/MoveToListModal.tsx new file mode 100644 index 000000000..b6683ad1e --- /dev/null +++ b/client/src/components/Collections/MoveToListModal.tsx @@ -0,0 +1,90 @@ +import React, { useMemo, useState } from 'react' +import { Search, ArrowRight, Copy, Loader2, Bookmark } from 'lucide-react' +import Modal from '../shared/Modal' +import type { Collection } from '@trek/shared' +import type { TranslationFn } from '../../types' + +interface MoveToListModalProps { + mode: 'move' | 'copy' + /** Candidate target lists (owned, excluding the current one). */ + lists: Collection[] + /** Number of selected places. */ + count: number + onPick: (targetId: number) => Promise | void + onClose: () => void + t: TranslationFn +} + +/** + * Target-list picker for moving or duplicating the selected places into another + * of the user's lists. `mode` only changes the wording + the trailing icon; the + * action itself is the parent's onPick. + */ +export default function MoveToListModal({ mode, lists, count, onPick, onClose, t }: MoveToListModalProps): React.ReactElement { + const [search, setSearch] = useState('') + const [busy, setBusy] = useState(null) + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase() + return q ? lists.filter(l => l.name.toLowerCase().includes(q)) : lists + }, [lists, search]) + + const pick = async (id: number) => { + if (busy != null) return + setBusy(id) + try { await onPick(id) } finally { setBusy(null) } + } + + const Trailing = mode === 'move' ? ArrowRight : Copy + + return ( + +
+ {lists.length > 3 && ( +
+ + setSearch(e.target.value)} + placeholder={t('collections.copyToTripSearch')} + className="w-full pl-8 pr-3 py-2 rounded-lg border border-edge bg-surface-input text-content text-[13px] outline-none focus:border-accent" + /> +
+ )} + {filtered.length === 0 ? ( +

{t('collections.noOtherLists')}

+ ) : ( +
+ {filtered.map(list => { + const isBusy = busy === list.id + return ( + + ) + })} +
+ )} +
+
+ ) +} diff --git a/client/src/pages/CollectionsPage.tsx b/client/src/pages/CollectionsPage.tsx index 1edc05a11..6319d829d 100644 --- a/client/src/pages/CollectionsPage.tsx +++ b/client/src/pages/CollectionsPage.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckSquare, X, Trash2, Copy, Plus } from 'lucide-react' +import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckCheck, X, Trash2, Copy, CopyPlus, FolderInput, Plus } from 'lucide-react' import Navbar from '../components/Layout/Navbar' import Modal from '../components/shared/Modal' import ListsRail from '../components/Collections/ListsRail' @@ -9,6 +9,7 @@ import CollectionList from '../components/Collections/CollectionList' import CollectionFilterBar from '../components/Collections/CollectionFilterBar' import CollectionMapPanel from '../components/Collections/CollectionMapPanel' import CopyToTripModal from '../components/Collections/CopyToTripModal' +import MoveToListModal from '../components/Collections/MoveToListModal' import ShareCollectionModal from '../components/Collections/ShareCollectionModal' import AddPlaceToCollectionModal from '../components/Collections/AddPlaceToCollectionModal' import CollectionPlaceDetail from '../components/Collections/CollectionPlaceDetail' @@ -105,6 +106,9 @@ export default function CollectionsPage(): React.ReactElement { categoryOptions={c.categoryOptions} onStatusFilter={c.setStatusFilter} onCategoryFilter={c.setCategoryFilter} + showSelect={showSelect} + selectMode={c.selectMode} + onToggleSelect={() => c.setSelectMode(!c.selectMode)} t={t} /> ) : null @@ -204,17 +208,6 @@ export default function CollectionsPage(): React.ReactElement { )} - {showSelect && ( - - )} {canAddPlace && ( {t('collections.selectedCount', { count: c.selectedIds.length })}
- - + +