Skip to content
This repository has been archived by the owner on Jul 26, 2023. It is now read-only.

Commit

Permalink
Merge pull request #283 from spencerwooo/native-search
Browse files Browse the repository at this point in the history
  • Loading branch information
spencerwooo authored Jan 22, 2022
2 parents fd95e03 + 3d918fb commit c0efb31
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 12 deletions.
16 changes: 11 additions & 5 deletions components/FileListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import VideoPreview from './previews/VideoPreview'
import PDFPreview from './previews/PDFPreview'
import URLPreview from './previews/URLPreview'
import DefaultPreview from './previews/DefaultPreview'
import { PreviewContainer } from './previews/Containers'
import { DownloadBtnContainer, PreviewContainer } from './previews/Containers'
import DownloadButtonGroup from './DownloadBtnGtoup'

// Disabling SSR for some previews (image gallery view, and PDF view)
const ReactViewer = dynamic(() => import('react-viewer'), { ssr: false })
Expand Down Expand Up @@ -545,10 +546,15 @@ const FileListing: FC<{ query?: ParsedUrlQuery }> = ({ query }) => {
switch (extensions[fileExtension]) {
case preview.image:
return (
<PreviewContainer>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="mx-auto" src={downloadUrl} alt={fileName} />
</PreviewContainer>
<>
<PreviewContainer>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="mx-auto" src={downloadUrl} alt={fileName} />
</PreviewContainer>
<DownloadBtnContainer>
<DownloadButtonGroup downloadUrl={file['@microsoft.graph.downloadUrl']} />
</DownloadBtnContainer>
</>
)

case preview.text:
Expand Down
35 changes: 32 additions & 3 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { IconName } from '@fortawesome/fontawesome-svg-core'
import { Dialog, Transition } from '@headlessui/react'
import toast, { Toaster } from 'react-hot-toast'
import { useHotkeys } from 'react-hotkeys-hook'

import Link from 'next/link'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { Fragment, useEffect, useState } from 'react'

import siteConfig from '../config/site.json'
import SearchModal from './SearchModal'
import useDeviceOS from '../utils/useDeviceOS'

const Navbar = () => {
const router = useRouter()
const os = useDeviceOS()

const [tokenPresent, setTokenPresent] = useState(false)
const [isOpen, setIsOpen] = useState(false)

const [searchOpen, setSearchOpen] = useState(false)
const openSearchBox = () => setSearchOpen(true)

useHotkeys(`${os === 'mac' ? 'cmd' : 'ctrl'}+k`, openSearchBox)

useEffect(() => {
const storedToken = () => {
for (const r of siteConfig.protectedRoutes) {
Expand Down Expand Up @@ -44,15 +54,34 @@ const Navbar = () => {
<div className="bg-white dark:bg-gray-900 dark:border-gray-500/30 sticky top-0 bg-opacity-80 border-b border-gray-900/10 backdrop-blur-md z-[100]">
<Toaster />

<div className="flex items-center justify-between w-full mx-auto px-4 py-1">
<SearchModal searchOpen={searchOpen} setSearchOpen={setSearchOpen} />

<div className="flex items-center space-x-4 justify-between w-full mx-auto px-4 py-1">
<Link href="/" passHref>
<a className="dark:text-white hover:opacity-80 flex items-center p-2 space-x-2">
<a className="dark:text-white hover:opacity-80 flex items-center py-2 md:p-2 space-x-2">
<Image src={siteConfig.icon} alt="icon" width="25" height="25" priority />
<span className="sm:block hidden font-bold">{siteConfig.title}</span>
</a>
</Link>

<div className="flex items-center space-x-4 text-gray-700">
<div className="flex items-center flex-1 md:flex-initial space-x-4 text-gray-700">
<button
className="flex-1 flex items-center justify-between rounded-lg bg-gray-100 dark:bg-gray-800 md:w-48 px-2.5 py-1.5 dark:text-white hover:opacity-80"
onClick={openSearchBox}
>
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon="search" />
<span className="text-sm font-medium">Search ...</span>
</div>

<div className="flex items-center space-x-1">
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">
{os === 'mac' ? '⌘' : 'Ctrl'}
</div>
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">K</div>
</div>
</button>

{siteConfig.links.length !== 0 &&
siteConfig.links.map((l: { name: string; link: string }) => (
<a
Expand Down
146 changes: 146 additions & 0 deletions components/SearchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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'

import { Dispatch, FC, Fragment, SetStateAction, useState } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import Link from 'next/link'

import { OdSearchResult } from '../types'
import { getFileIcon } from '../utils/getFileIcon'
import siteConfig from '../config/site.json'
import { LoadingIcon } from './Loading'

function useDriveItemSearch() {
const [query, setQuery] = useState('')
const searchDriveItem = async (q: string) => {
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]
}

// Map parentReference to the absolute path of the search result
data.map(item => {
item['path'] = `${mapAbsolutePath(item.parentReference.path)}/${encodeURIComponent(item.name)}`
})

return data
}

const debouncedNotionSearch = useConstant(() => AwesomeDebouncePromise(searchDriveItem, 1000))
const results = useAsync(async () => {
if (query.length === 0) {
return []
} else {
return debouncedNotionSearch(query)
}
}, [query])

return {
query,
setQuery,
results,
}
}

function SearchModal({
searchOpen,
setSearchOpen,
}: {
searchOpen: boolean
setSearchOpen: Dispatch<SetStateAction<boolean>>
}) {
const closeSearchBox = () => setSearchOpen(false)

const { query, setQuery, results } = useDriveItemSearch()

return (
<Transition appear show={searchOpen} as={Fragment}>
<Dialog as="div" className="inset-0 z-[200] fixed overflow-y-auto" onClose={closeSearchBox}>
<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"
>
<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">
<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" />
<input
type="text"
id="search-box"
className="w-full bg-transparent focus:outline-none focus-visible:outline-none"
placeholder="Search ..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
<div className="px-2 py-1 rounded-lg bg-gray-100 dark:bg-gray-700 font-medium text-xs">ESC</div>
</Dialog.Title>

<div className="bg-white dark:text-white dark:bg-gray-900 max-h-[80vh] overflow-x-hidden overflow-y-scroll">
{results.loading && (
<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>
</div>
)}
{results.error && (
<div className="text-center px-4 py-12 text-sm font-medium">Error: {results.error.message}</div>
)}
{results.result && (
<>
{results.result.length === 0 ? (
<div className="text-center px-4 py-12 text-sm font-medium">Nothing here.</div>
) : (
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>
</div>
</Link>
))
)}
</>
)}
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition>
)
}

export default SearchModal
Loading

1 comment on commit c0efb31

@vercel
Copy link

@vercel vercel bot commented on c0efb31 Jan 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.