diff --git a/src/@/components/BookmarkForm.tsx b/src/@/components/BookmarkForm.tsx index f3ddb46..aee2ba2 100644 --- a/src/@/components/BookmarkForm.tsx +++ b/src/@/components/BookmarkForm.tsx @@ -24,9 +24,12 @@ import { checkLinkExists, postLink } from '../lib/actions/links.ts'; import { AxiosError } from 'axios'; import { toast } from '../../hooks/use-toast.ts'; import { Toaster } from './ui/Toaster.tsx'; -import { getCollections } from '../lib/actions/collections.ts'; +import { + getCollections, + createCollection, +} from '../lib/actions/collections.ts'; import { getTags } from '../lib/actions/tags.ts'; -import { ExternalLink, X } from 'lucide-react'; +import { ExternalLink, X, Plus } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from './ui/Popover.tsx'; import { CaretSortIcon } from '@radix-ui/react-icons'; import { @@ -44,6 +47,10 @@ const BookmarkForm = () => { const [openCollections, setOpenCollections] = useState(false); const [uploadImage, setUploadImage] = useState(false); const [state, setState] = useState<'capturing' | 'uploading' | null>(null); + const [pendingNewCollection, setPendingNewCollection] = useState< + string | null + >(null); + const [searchValue, setSearchValue] = useState(''); const [isConfigured, setIsConfigured] = useState(false); const [isDuplicate, setIsDuplicate] = useState(false); @@ -82,10 +89,29 @@ const BookmarkForm = () => { const { mutate: onSubmit, isLoading } = useMutation({ mutationFn: async (values: bookmarkFormValues) => { + let finalValues = values; + + if (pendingNewCollection) { + const response = await createCollection( + config?.baseUrl as string, + config?.apiKey as string, + pendingNewCollection + ); + const newCollection = response.data.response; + finalValues = { + ...values, + collection: { + id: newCollection.id, + ownerId: newCollection.ownerId, + name: newCollection.name, + }, + }; + } + await postLink( config?.baseUrl as string, uploadImage, - values, + finalValues, setState, config?.apiKey as string ); @@ -94,19 +120,20 @@ const BookmarkForm = () => { }, onError: (error) => { console.error(error); + const isCollectionCreationError = pendingNewCollection !== null; + const fallbackMessage = isCollectionCreationError + ? 'There was an error while trying to create the collection. Please try again.' + : 'There was an error while trying to save the link. Please try again.'; if (error instanceof AxiosError) { toast({ title: 'Error', - description: - error.response?.data.response || - 'There was an error while trying to save the link. Please try again.', + description: error.response?.data.response || fallbackMessage, variant: 'destructive', }); } else { toast({ title: 'Error', - description: - 'There was an error while trying to save the link. Please try again.', + description: fallbackMessage, variant: 'destructive', }); } @@ -151,7 +178,7 @@ const BookmarkForm = () => { }; setTabInformation(); - }, []); + }, [form]); const { handleSubmit, control } = form; @@ -195,6 +222,19 @@ const BookmarkForm = () => { enabled: isConfigured, }); + const trimmedSearch = searchValue.trim(); + const hasExactMatch = + trimmedSearch && + collections?.some( + (c: { name: string }) => + c.name.toLowerCase() === trimmedSearch.toLowerCase() + ); + const hasPartialMatch = + trimmedSearch && + collections?.some((c: { name: string }) => + c.name.toLowerCase().includes(trimmedSearch.toLowerCase()) + ); + const { isLoading: loadingTags, data: tags, @@ -214,6 +254,125 @@ const BookmarkForm = () => { enabled: isConfigured, }); + const renderCollectionCommand = (extraClassName?: string) => ( + { + if (value.toLowerCase().includes(search.trim().toLowerCase())) return 1; + return 0; + }} + > + + + {loadingCollections ? ( +

Loading...

+ ) : ( + <> + + {!trimmedSearch || hasPartialMatch ? ( + 'No collection found.' + ) : ( +
+ No collection found. + +
+ )} +
+ {Array.isArray(collections) && ( + + {isLoading ? ( + { + form.setValue('collection', { name: 'Unorganized' }); + setPendingNewCollection(null); + setSearchValue(''); + setOpenCollections(false); + }} + > + Unorganized + + ) : ( + collections?.map( + (collection: { + name: string; + id: number; + ownerId: number; + pathname: string; + }) => ( + { + form.setValue('collection', { + ownerId: collection.ownerId, + id: collection.id, + name: collection.name, + }); + setPendingNewCollection(null); + setSearchValue(''); + setOpenCollections(false); + }} + > +

{collection.name}

+

+ {collection.pathname} +

+
+ ) + ) + )} +
+ )} + + )} +
+ ); + + const renderCreateFooter = () => { + if (!trimmedSearch || hasExactMatch || !hasPartialMatch) return null; + return ( +
{ + form.setValue('collection', { name: trimmedSearch }); + setPendingNewCollection(trimmedSearch); + setSearchValue(''); + setOpenCollections(false); + }} + > + +
+ + Create "{trimmedSearch}" + + + New collection + +
+
+ ); + }; + return (
@@ -235,7 +394,10 @@ const BookmarkForm = () => {
{ + setOpenCollections(open); + if (!open) setSearchValue(''); + }} > @@ -247,14 +409,16 @@ const BookmarkForm = () => { 'w-full justify-between bg-neutral-100 dark:bg-neutral-900' } > - {loadingCollections - ? 'Unorganized' - : field.value?.name - ? collections?.find( - (collection: { name: string }) => - collection.name === field.value?.name - )?.name || form.getValues('collection')?.name - : 'Select a collection...'} + {pendingNewCollection + ? `${pendingNewCollection} (new)` + : loadingCollections + ? 'Unorganized' + : field.value?.name + ? collections?.find( + (collection: { name: string }) => + collection.name === field.value?.name + )?.name || form.getValues('collection')?.name + : 'Select a collection...'} @@ -262,138 +426,36 @@ const BookmarkForm = () => { {!openOptions && openCollections ? (
- - - - {loadingCollections ? ( -

- Loading... -

- ) : ( - <> - No Collection found. - {Array.isArray(collections) && ( - - {isLoading ? ( - { - form.setValue('collection', { - name: 'Unorganized', - }); - setOpenCollections(false); - }} - > - Unorganized - - ) : ( - collections?.map( - (collection: { - name: string; - id: number; - ownerId: number; - pathname: string; - }) => ( - { - form.setValue('collection', { - ownerId: collection.ownerId, - id: collection.id, - name: collection.name, - }); - setOpenCollections(false); - }} - > -

{collection.name}

-

- {collection.pathname} -

-
- ) - ) - )} -
- )} - - )} -
+ {renderCollectionCommand('rounded-none')} + {renderCreateFooter()}
) : openOptions && openCollections ? ( - - - No Collection found. - {Array.isArray(collections) && ( - - {isLoading ? ( - { - form.setValue('collection', { - name: 'Unorganized', - }); - setOpenCollections(false); - }} - > - Unorganized - - ) : ( - collections?.map( - (collection: { - name: string; - id: number; - ownerId: number; - pathname: string; - }) => ( - { - form.setValue('collection', { - ownerId: collection.ownerId, - id: collection.id, - name: collection.name, - }); - setOpenCollections(false); - }} - > -

{collection.name}

-

- {collection.pathname} -

-
- ) - ) - )} -
- )} -
+ {renderCollectionCommand()} + {renderCreateFooter()}
) : undefined}
diff --git a/src/@/lib/actions/collections.ts b/src/@/lib/actions/collections.ts index 27754a5..436e940 100644 --- a/src/@/lib/actions/collections.ts +++ b/src/@/lib/actions/collections.ts @@ -60,3 +60,26 @@ export async function getCollections(baseUrl: string, apiKey: string) { }, }; } + +export async function createCollection( + baseUrl: string, + apiKey: string, + name: string +) { + const url = `${baseUrl}/api/v1/collections`; + + const response = await axios.post< + { response: ResponseCollections } + >( + url, + { name }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + } + ); + + return response; +}