diff --git a/client/src/components/Collections/AddPlaceToCollectionModal.tsx b/client/src/components/Collections/AddPlaceToCollectionModal.tsx index 392ca15fa..1a95a4375 100644 --- a/client/src/components/Collections/AddPlaceToCollectionModal.tsx +++ b/client/src/components/Collections/AddPlaceToCollectionModal.tsx @@ -135,7 +135,7 @@ export default function AddPlaceToCollectionModal({ isOpen, collectionId, collec {results.map((r, i) => ( ) diff --git a/client/src/components/Collections/ListEditorModal.tsx b/client/src/components/Collections/ListEditorModal.tsx index bbdab3f45..2b0f5cf86 100644 --- a/client/src/components/Collections/ListEditorModal.tsx +++ b/client/src/components/Collections/ListEditorModal.tsx @@ -1,13 +1,22 @@ import React, { useEffect, useRef, useState } from 'react' -import { ImagePlus, Link2, Plus, Trash2 } from 'lucide-react' +import { ImagePlus, Link2, Plus, Trash2, Search, Loader2 } from 'lucide-react' import Modal from '../shared/Modal' import { useCollectionStore } from '../../store/collectionStore' import { useToast } from '../shared/Toast' +import { tripsApi } from '../../api/client' import { getApiErrorMessage } from '../../types' import { normalizeLinkUrl } from '../../pages/collections/collectionsModel' import type { TranslationFn } from '../../types' import type { Collection, CollectionLink } from '@trek/shared' +interface CoverSearchPhoto { + id: string + url: string + thumb: string + description?: string | null + photographer?: string | null +} + const SWATCHES = ['#6366f1', '#ec4899', '#14b8a6', '#f97316', '#8b5cf6', '#ef4444', '#3b82f6', '#22c55e'] interface ListEditorModalProps { @@ -39,6 +48,11 @@ export default function ListEditorModal({ target, onClose, onCreated, onRequestD const [links, setLinks] = useState([]) const [coverFile, setCoverFile] = useState(null) const [coverPreview, setCoverPreview] = useState(null) + const [pendingUnsplashUrl, setPendingUnsplashUrl] = useState(null) + const [coverQuery, setCoverQuery] = useState('') + const [coverResults, setCoverResults] = useState([]) + const [searchingCover, setSearchingCover] = useState(false) + const coverSeq = useRef(0) const [saving, setSaving] = useState(false) // Remembers a freshly created list id so a retry after a cover-upload failure // updates it instead of creating a duplicate list. @@ -57,6 +71,9 @@ export default function ListEditorModal({ target, onClose, onCreated, onRequestD setCoverFile(null) dropObjectUrl() setCoverPreview(editing?.cover_image ?? null) + setPendingUnsplashUrl(null) + setCoverQuery('') + setCoverResults([]) setCreatedId(null) // eslint-disable-next-line react-hooks/exhaustive-deps }, [target]) @@ -73,6 +90,31 @@ export default function ListEditorModal({ target, onClose, onCreated, onRequestD objectUrl.current = url setCoverFile(file) setCoverPreview(url) + setPendingUnsplashUrl(null) // an uploaded file wins over a picked Unsplash photo + } + + const searchCover = async () => { + const query = coverQuery.trim() || name.trim() + if (!query) return + const seq = ++coverSeq.current + setSearchingCover(true) + try { + const data = await tripsApi.searchCoverImages(query) + if (seq !== coverSeq.current) return + setCoverResults(data.photos || []) + } catch { + if (seq === coverSeq.current) setCoverResults([]) + } finally { + if (seq === coverSeq.current) setSearchingCover(false) + } + } + + const pickUnsplash = (photo: CoverSearchPhoto) => { + if (!photo.url) return + dropObjectUrl() + setCoverFile(null) + setPendingUnsplashUrl(photo.url) + setCoverPreview(photo.url) } const setLink = (i: number, patch: Partial) => @@ -85,7 +127,7 @@ export default function ListEditorModal({ target, onClose, onCreated, onRequestD const cleanLinks = links .map(l => ({ label: l.label?.trim() || undefined, url: normalizeLinkUrl(l.url) })) .filter(l => l.url) - const payload = { name: trimmed, color, description: description.trim() || null, links: cleanLinks } + const payload = { name: trimmed, color, description: description.trim() || null, links: cleanLinks, ...(pendingUnsplashUrl ? { cover_image: pendingUnsplashUrl } : {}) } setSaving(true) try { // A prior create that failed at the cover step left `createdId` set — reuse @@ -152,6 +194,44 @@ export default function ListEditorModal({ target, onClose, onCreated, onRequestD pickCover(e.target.files?.[0])} /> + + {/* Unsplash cover search — same source as trip creation */} +
+ setCoverQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); searchCover() } }} + placeholder={t('dashboard.unsplashSearchPlaceholder')} + className="flex-1 min-w-0 px-3 py-2 rounded-lg border border-edge bg-surface-input text-content text-[13px] outline-none focus:border-accent" + /> + +
+ {coverResults.length > 0 && ( +
+ {coverResults.map(photo => ( + + ))} +
+ )} {/* Name */}