diff --git a/src/components/npm-stats/PackageSearch.tsx b/src/components/npm-stats/PackageSearch.tsx index 4ac1fd0d..7277331c 100644 --- a/src/components/npm-stats/PackageSearch.tsx +++ b/src/components/npm-stats/PackageSearch.tsx @@ -3,16 +3,18 @@ import { useDebouncedValue } from '@tanstack/react-pacer' import { Search } from 'lucide-react' import { keepPreviousData, useQuery } from '@tanstack/react-query' import { Command } from 'cmdk' +import { twMerge } from 'tailwind-merge' import { Spinner } from '~/components/Spinner' type NpmSearchResult = { name: string description?: string version?: string - label?: string publisher?: { username?: string } } +const CREATE_ITEM_VALUE = '__create__' + export type PackageSearchProps = { onSelect: (packageName: string) => void placeholder?: string @@ -48,110 +50,123 @@ export function PackageSearch({ } }, []) + const hasUsableQuery = debouncedInputValue.length > 2 + const searchQuery = useQuery({ queryKey: ['npm-search', debouncedInputValue], queryFn: async () => { - if (!debouncedInputValue || debouncedInputValue.length <= 2) - return [] as Array - const response = await fetch( - `https://api.npms.io/v2/search?q=${encodeURIComponent( + `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent( debouncedInputValue, )}&size=10`, ) const data = (await response.json()) as { - results: Array<{ package: NpmSearchResult }> + objects: Array<{ package: NpmSearchResult }> } - return data.results.map((r) => r.package) + return data.objects.map((r) => r.package) }, - enabled: debouncedInputValue.length > 2, + enabled: hasUsableQuery, placeholderData: keepPreviousData, }) - const results = React.useMemo(() => { - const hasInputValue = searchQuery.data?.find( - (d) => d.name === debouncedInputValue, - ) - - return [ - ...(hasInputValue - ? [] - : [ - { - name: debouncedInputValue, - label: `Use "${debouncedInputValue}"`, - }, - ]), - ...(searchQuery.data ?? []), - ] - }, [searchQuery.data, debouncedInputValue]) - - const handleInputChange = (value: string) => { - setInputValue(value) - } + const searchResults = hasUsableQuery ? (searchQuery.data ?? []) : [] + const trimmedInput = debouncedInputValue.trim() + const showCreateItem = + hasUsableQuery && + trimmedInput.length > 0 && + !searchResults.some((d) => d.name === trimmedInput) const handleSelect = (value: string) => { - const selectedItem = results?.find((item) => item.name === value) - if (!selectedItem) return - - onSelect(selectedItem.name) + if (value === CREATE_ITEM_VALUE) { + if (!trimmedInput) return + onSelect(trimmedInput) + } else { + const match = searchResults.find((item) => item.name === value) + if (!match) return + onSelect(match.name) + } setInputValue('') setOpen(false) } + const showList = open && inputValue.length > 0 + return ( -
-
- -
- - setOpen(true)} - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={autoFocus} - /> -
+
+ +
+ + setOpen(true)} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={autoFocus} + /> {searchQuery.isFetching && ( -
+
)} - {inputValue.length && open ? ( - - {inputValue.length < 3 ? ( -
Keep typing to search...
- ) : searchQuery.isLoading ? ( -
- Searching... +
+ + {inputValue.length < 3 ? ( +
+ Keep typing to search... +
+ ) : searchQuery.isLoading ? ( +
+ Searching... +
+ ) : !searchResults.length && !showCreateItem ? ( +
+ No packages found +
+ ) : null} + {showCreateItem && ( + +
Use "{trimmedInput}"
+
+ )} + {searchResults.map((item) => ( + +
{item.name}
+ {item.description ? ( +
+ {item.description}
- ) : !results?.length ? ( -
No packages found
) : null} - {results?.map((item) => ( - -
{item.label || item.name}
-
- {item.description} -
-
- {item.version ? `v${item.version}• ` : ''} - {item.publisher?.username} -
-
- ))} -
- ) : null} - -
+
+ {item.version ? `v${item.version}` : ''} + {item.version && item.publisher?.username ? ' • ' : ''} + {item.publisher?.username} +
+ + ))} + +
) } diff --git a/src/routes/stats/npm/index.tsx b/src/routes/stats/npm/index.tsx index 7064efc1..3d1062ad 100644 --- a/src/routes/stats/npm/index.tsx +++ b/src/routes/stats/npm/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { Link, createFileRoute } from '@tanstack/react-router' import * as v from 'valibot' import { useThrottledCallback } from '@tanstack/react-pacer' +import * as DialogPrimitive from '@radix-ui/react-dialog' import { X } from 'lucide-react' import { useQuery } from '@tanstack/react-query' import { Card } from '~/components/Card' @@ -601,29 +602,38 @@ function RouteComponent() { /> {/* Combine Package Dialog */} - {combiningPackage && ( -
-
-
-

+ { + if (!open) setCombiningPackage(null) + }} + > + + + +
+ Add packages to {combiningPackage} -

- +
- -
-
- )} + + Search for additional npm packages to combine with{' '} + {combiningPackage}. + + {combiningPackage && ( + + )} + + + {/* Color Picker Popover */} {colorPickerPackage && colorPickerPosition && (