feat: search components.

This commit is contained in:
Hossein Mehrabi
2022-10-09 04:43:29 +03:30
parent 21411f5c19
commit de42c8ff78
41 changed files with 1325 additions and 17 deletions
+4 -1
View File
@@ -18,5 +18,8 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yalc
yalc.lock
.idea
static/search-index.json
+14 -2
View File
@@ -10,7 +10,7 @@ const config = {
tagline: 'Dinosaurs are cool',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenLinks: 'warn',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
@@ -127,7 +127,19 @@ const config = {
darkTheme: darkCodeTheme,
},
}),
plugins: ['docusaurus-plugin-sass'],
plugins: [
'docusaurus-plugin-sass',
[
'@acid-info/docusaurus-search-local',
{
hashed: true,
indexDocs: true,
indexBlog: false,
indexPages: true,
docsRouteBasePath: '/',
},
],
],
}
module.exports = config
+5 -1
View File
@@ -18,18 +18,22 @@
"prettier:staged": "pretty-quick --staged"
},
"dependencies": {
"@acid-info/docusaurus-search-local": "^0.32.1",
"@docusaurus/core": "2.1.0",
"@docusaurus/preset-classic": "2.1.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.2",
"lodash": "^4.17.21",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-use": "^17.4.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.1.0",
"@tsconfig/docusaurus": "^1.0.5",
"@types/lodash": "^4.14.186",
"docusaurus-plugin-sass": "^0.2.2",
"husky": "^8.0.1",
"prettier": "^2.7.1",
+4
View File
@@ -1,3 +1,7 @@
declare module '@docusaurus/theme-common/internal' {
export * from '@docusaurus/theme-common/lib/internal'
}
declare module '@docusaurus/plugin-content-docs/client' {
export * from '@docusaurus/plugin-content-docs/lib/client'
}
+18
View File
@@ -0,0 +1,18 @@
export const settle = async <R, E = Error>(
promise: Promise<R>,
): Promise<[R, undefined] | [undefined, E]> => {
try {
const result: R = await promise
return [result, undefined]
} catch (error) {
return [undefined, error as E]
}
}
export const settleSync = <R, E = Error>(func: () => R): [R, E] => {
try {
return [func(), null]
} catch (error) {
return [undefined, error]
}
}
+28
View File
@@ -0,0 +1,28 @@
import { useEffect, useMemo } from 'react'
export const useEventListener = <
T = any,
E extends { addEventListener: any; removeEventListener: any } = any,
>(
key: string | (() => string),
element: E | (() => E),
listener: (event: T) => void,
options?: boolean | AddEventListenerOptions,
deps?: any[],
) => {
const _key = useMemo(() => (typeof key === 'string' ? key : key()), [])
const _element = useMemo(
() => (typeof element === 'function' ? element() : element),
[element],
)
useEffect(() => {
if (!_element?.addEventListener || !_element?.removeEventListener) return
_element.addEventListener(_key, listener, options)
return () => {
_element.removeEventListener(_key, listener, options)
}
}, [_key, _element])
}
+12
View File
@@ -0,0 +1,12 @@
export const useOS = (): 'windows' | 'linux' | 'mac' | 'unknown' => {
const str =
navigator?.appVersion ?? navigator?.platform ?? navigator?.userAgent
return /mac/gi.test(str)
? 'mac'
: /win/gi.test(str)
? 'windows'
: /linux/gi.test(str)
? 'linux'
: 'unknown'
}
+20
View File
@@ -0,0 +1,20 @@
import { useEventListener } from './useEventListener'
export const useWindowEventListener = <K extends keyof WindowEventMap, T = any>(
key: K | string,
listener: (
ev: Record<K, any> extends Record<K, WindowEventMap[K]>
? WindowEventMap[K]
: T,
) => void,
options?: boolean | AddEventListenerOptions,
deps: any[] = [],
) => {
useEventListener(
key,
() => typeof window !== 'undefined' && window,
listener,
options,
deps,
)
}
+1
View File
@@ -1,6 +1,7 @@
import { globalStore } from '@site/src/containers/GlobalStore'
import Layout from '@theme-original/Layout'
import React from 'react'
import './styles.scss'
export default function LayoutWrapper(props) {
const store = globalStore.useCreateStore({ hiddenSidebar: false })
+13
View File
@@ -0,0 +1,13 @@
// TODO: move styles to css/custom.scss file.
.hidden-scrollbar {
/* Chrome, Safari and Opera */
&::-webkit-scrollbar {
width: 0;
display: none;
}
/* Firefox, Edge and IE */
-ms-overflow-style: none;
scrollbar-width: none;
}
+1 -1
View File
@@ -69,7 +69,7 @@ export default function NavbarContent(): JSX.Element {
<div className={styles.navbarLogoWrapper}>
<NavbarLogo />
</div>
<div>
<div className={styles.searchContainer}>
<NavbarSearch>
<SearchBar />
</NavbarSearch>
@@ -105,6 +105,10 @@
}
}
.searchContainer > div {
padding: 0;
}
@include utils.responsive('lg', 'down') {
.root {
padding: 0 var(--ifm-spacing-horizontal);
@@ -0,0 +1,3 @@
.root {
position: relative;
}
+82
View File
@@ -0,0 +1,82 @@
import { useLocation } from '@docusaurus/router'
import clsx from 'clsx'
import React, { useEffect, useRef, useState } from 'react'
import { useSearch } from './hooks/useSearch'
import styles from './SearchBar.module.scss'
import { SearchInput } from './SearchInput'
import { SearchResults } from './SearchResults'
import { SearchResultsContainer } from './SearchResultsContainer'
import { SearchResult } from './types'
export const SearchBar: React.FC<{}> = ({}) => {
const search = useSearch()
const ref = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const location = useLocation()
const [input, setInput] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [showResultsContainer, setShowResultsContainer] = useState(false)
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const onInputFocus = () => {
setShowResultsContainer(true)
}
const onInputCancel = () => {
setShowResultsContainer(false)
}
const onClickOutsideResultsContainer = (event: Event) => {
if (!event.composedPath().find((el) => el === ref.current)) onInputCancel()
}
const onClear = () => {
setInput('')
inputRef.current.focus()
}
const query = async (input: string) => {
const { results } = await search.query(input)
setResults(results)
setShowResultsContainer(true)
}
useEffect(() => {
if (input.length > 0) query(input)
else setResults([])
}, [input])
useEffect(() => {
if (showResultsContainer) {
setShowResultsContainer(false)
}
}, [location.key])
return (
<div ref={ref} className={clsx(styles.root)}>
<SearchInput
value={input}
active={showResultsContainer}
inputProps={{
ref: inputRef,
placeholder: 'Enter...',
}}
onChange={onInputChange}
onFocus={onInputFocus}
onCancel={onInputCancel}
/>
<SearchResultsContainer
visible={showResultsContainer && input.length > 0}
inputRef={inputRef}
onClickOutside={onClickOutsideResultsContainer}
>
<SearchResults results={results} onClear={onClear} />
</SearchResultsContainer>
</div>
)
}
@@ -0,0 +1,74 @@
// TODO: use ifm variables
.root {
width: auto;
position: relative;
}
.root input {
width: 134px;
border: 0;
border-radius: 8px;
background: var(--ifm-background-surface-color);
padding: 9px 48px 9px 16px;
transition: 0.2s;
&:focus {
outline: none;
}
}
.root.expanded input {
background: #eeeef0;
width: 334px;
}
.root.expanded .label {
display: none;
}
.label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #747475;
font-size: 0.875rem;
pointer-events: none;
}
.shortcuts {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
line-height: 0;
pointer-events: none;
& > kbd {
margin-left: 0.375rem;
}
kbd {
background: none;
border: none;
padding: 0;
color: #bababa;
box-shadow: none;
font-size: 0.75rem;
}
}
[data-theme='dark'] {
.root input {
color: currentColor;
}
.root.expanded input {
background-color: var(--ifm-background-color);
}
}
@@ -0,0 +1,84 @@
import { useOS } from '@site/src/lib/useOS'
import { useWindowEventListener } from '@site/src/lib/useWindowEventListener'
import clsx from 'clsx'
import React from 'react'
import {} from 'react-use'
import styles from './SearchInput.module.scss'
export type SearchInputProps = Omit<
React.HTMLProps<HTMLDivElement>,
'value' | 'onChange'
> &
Pick<React.HTMLProps<HTMLInputElement>, 'onChange'> & {
inputProps?: Omit<React.HTMLProps<HTMLInputElement>, 'ref'> & {
ref: React.MutableRefObject<HTMLInputElement>
}
value?: string
active?: boolean
onFocus?: () => void
onCancel?: () => void
}
export const SearchInput: React.FC<SearchInputProps> = ({
value,
active,
onChange,
onFocus: onFocusCallback,
onCancel,
className,
inputProps: { ref: inputRef, ...inputProps },
...props
}) => {
const os = useOS()
const expanded = active || value?.length > 0
const focus = () => {
inputRef.current.focus()
}
const blur = () => {
inputRef.current.blur()
onCancel && onCancel()
}
useWindowEventListener('keydown', (event) => {
if (event.ctrlKey && event.code === 'KeyK') {
event.preventDefault()
focus()
} else if (event.code === 'Escape') {
blur()
}
})
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
onFocusCallback && onFocusCallback()
}
return (
<div
className={clsx(styles.root, expanded && styles.expanded, className)}
{...props}
>
<span className={styles.label}>Search</span>
<input
value={value}
onChange={onChange}
ref={inputRef}
onFocus={onFocus}
{...inputProps}
placeholder={expanded ? inputProps.placeholder : ''}
/>
<div className={styles.shortcuts}>
{active ? (
<kbd>esc</kbd>
) : (
<>
<kbd>{os === 'mac' ? '⌘' : 'ctrl'}</kbd>
<kbd>k</kbd>
</>
)}
</div>
</div>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './SearchInput'
@@ -0,0 +1,17 @@
.root {
& > div:first-child {
padding: 0 14px;
font-size: 0.75rem;
font-weight: bold;
color: #828285;
}
ul {
margin-top: 16px;
padding: 0;
}
ul li {
list-style: none;
}
}
@@ -0,0 +1,23 @@
import clsx from 'clsx'
import React from 'react'
import styles from './SearchResultGroup.module.scss'
export type SearchResultGroupProps = React.HTMLProps<HTMLDivElement> & {
title: string
}
export const SearchResultGroup: React.FC<SearchResultGroupProps> = ({
className,
title = '',
children,
...props
}) => {
return (
<div className={clsx(styles.root, className)} {...props}>
<div>
<span>{title}</span>
</div>
<ul>{children}</ul>
</div>
)
}
@@ -0,0 +1 @@
export * from './SearchResultGroup'

Some files were not shown because too many files have changed in this diff Show More