onedrive/components/SearchModal.tsx

154 lines
6 KiB
TypeScript
Raw Normal View History

2022-01-16 16:08:04 +00:00
import axios from 'axios'
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { useAsync } from 'react-async-hook'
import useConstant from 'use-constant'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2022-01-16 16:08:04 +00:00
import { Dispatch, FC, Fragment, SetStateAction, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import Link from 'next/link'
2022-01-21 13:42:21 +00:00
import { OdSearchResult } from '../types'
import { getFileIcon } from '../utils/getFileIcon'
import siteConfig from '../config/site.json'
import { LoadingIcon } from './Loading'
function useDriveItemSearch() {
2022-01-16 16:08:04 +00:00
const [query, setQuery] = useState('')
const searchDriveItem = async (q: string) => {
2022-01-21 13:42:21 +00:00
const { data } = await axios.get<OdSearchResult>(`/api/search?q=${q}`)
// Extract the searched item's path and convert it to the absolute path in onedrive-vercel-index
function mapAbsolutePath(path: string): string {
return siteConfig.baseDirectory === '/' ? path.split('root:')[1] : path.split(siteConfig.baseDirectory)[1]
}
2022-01-16 16:08:04 +00:00
2022-01-21 13:42:21 +00:00
// Map parentReference to the absolute path of the search result
data.map(item => {
// TODO: supporting sharepoint search where the path is not returned in parentReference
if ('path' in item.parentReference) {
item['path'] = `${mapAbsolutePath(item.parentReference.path)}/${encodeURIComponent(item.name)}`
} else {
throw Error(
'We currently only support search in OneDrive international. SharePoint instances are not supported yet. See issue: https://github.com/spencerwooo/onedrive-vercel-index/issues/299'
)
}
2022-01-21 13:42:21 +00:00
})
return data
2022-01-16 16:08:04 +00:00
}
const debouncedNotionSearch = useConstant(() => AwesomeDebouncePromise(searchDriveItem, 1000))
const results = useAsync(async () => {
if (query.length === 0) {
return []
} else {
return debouncedNotionSearch(query)
}
}, [query])
return {
query,
setQuery,
results,
}
}
2022-01-21 13:42:21 +00:00
function SearchModal({
searchOpen,
setSearchOpen,
}: {
2022-01-16 16:08:04 +00:00
searchOpen: boolean
setSearchOpen: Dispatch<SetStateAction<boolean>>
2022-01-21 13:42:21 +00:00
}) {
2022-01-16 16:08:04 +00:00
const closeSearchBox = () => setSearchOpen(false)
const { query, setQuery, results } = useDriveItemSearch()
return (
<Transition appear show={searchOpen} as={Fragment}>
2022-01-21 14:12:07 +00:00
<Dialog as="div" className="inset-0 z-[200] fixed overflow-y-auto" onClose={closeSearchBox}>
2022-01-16 16:08:04 +00:00
<div className="min-h-screen text-center px-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="bg-white/80 inset-0 fixed dark:bg-gray-800/80" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="ease-out duration-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
2022-01-21 14:12:07 +00:00
<div className="border rounded border-gray-400/30 shadow-xl my-12 text-left w-full max-w-3xl transform transition-all inline-block overflow-hidden">
2022-01-21 13:42:21 +00:00
<Dialog.Title
as="h3"
className="flex items-center space-x-4 dark:text-white border-b bg-gray-50 border-gray-400/30 p-4 dark:bg-gray-800"
>
<FontAwesomeIcon icon="search" className="w-4 h-4" />
2022-01-16 16:08:04 +00:00
<input
type="text"
id="search-box"
2022-01-21 13:42:21 +00:00
className="w-full bg-transparent focus:outline-none focus-visible:outline-none"
2022-01-16 16:08:04 +00:00
placeholder="Search ..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
2022-01-21 13:42:21 +00:00
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">ESC</div>
2022-01-16 16:08:04 +00:00
</Dialog.Title>
2022-01-21 14:12:07 +00:00
<div className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll">
2022-01-16 16:08:04 +00:00
{results.loading && (
2022-01-21 13:42:21 +00:00
<div className="text-center px-4 py-12 text-sm font-medium">
<LoadingIcon className="animate-spin w-4 h-4 mr-2 inline-block svg-inline--fa" />
<span>Loading ...</span>
2022-01-16 16:08:04 +00:00
</div>
)}
{results.error && (
2022-01-21 13:42:21 +00:00
<div className="text-center px-4 py-12 text-sm font-medium">Error: {results.error.message}</div>
2022-01-16 16:08:04 +00:00
)}
{results.result && (
<>
{results.result.length === 0 ? (
2022-01-21 13:42:21 +00:00
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
2022-01-16 16:08:04 +00:00
) : (
2022-01-21 13:42:21 +00:00
results.result.map(result => (
<Link href={result.path} key={result.id} passHref>
<div
className="flex items-center space-x-4 border-b cursor-pointer border-gray-400/30 px-4 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-850"
onClick={closeSearchBox}
>
<FontAwesomeIcon icon={result.file ? getFileIcon(result.name) : ['far', 'folder']} />
<div>
<div className="text-sm font-medium leading-8">{result.name}</div>
<div className="text-xs font-mono opacity-60 truncate overflow-hidden">
{decodeURIComponent(result.path)}
</div>
</div>
2022-01-16 16:08:04 +00:00
</div>
</Link>
))
)}
</>
)}
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
)
}
export default SearchModal