Add new dropdown search
This commit is contained in:
parent
e811b4896e
commit
dc35932027
24
src/components/Book.css
Normal file
24
src/components/Book.css
Normal file
@ -0,0 +1,24 @@
|
||||
.book {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 5px 10px;
|
||||
}
|
||||
|
||||
.book:active {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.author {
|
||||
flex: 1;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.language {
|
||||
font-size: 0.7em;
|
||||
}
|
23
src/components/Book.js
Normal file
23
src/components/Book.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
import { setText, setLang } from '../store/actions'
|
||||
import { getBook } from '../lib/gutenberg'
|
||||
|
||||
import styles from './Book.css'
|
||||
|
||||
export const Book = ({ author, language, rights, subject, title, id }) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const handleClick = async () => {
|
||||
dispatch(setText(await getBook(id)))
|
||||
dispatch(setLang(language[0]))
|
||||
}
|
||||
return (
|
||||
<div className={styles.book} onClick={handleClick}>
|
||||
<div className={styles.title}>{title.join(' – ')}</div>
|
||||
<div className={styles.author}>{author[0]}</div>
|
||||
<div className={styles.language}>{language.join(' – ')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
20
src/components/SearchBar.css
Normal file
20
src/components/SearchBar.css
Normal file
@ -0,0 +1,20 @@
|
||||
.search {
|
||||
width: 300px;
|
||||
transition: width 0.2s ease-in-out;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.search:focus {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.label svg {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
81
src/components/SearchBar.js
Normal file
81
src/components/SearchBar.js
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { debounce } from 'debounce'
|
||||
import classNames from 'classnames'
|
||||
import { FiSearch } from 'react-icons/fi'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import axios from 'axios'
|
||||
|
||||
import { search } from '../lib/gutenberg'
|
||||
import { Spinner } from '../styles/Spinner'
|
||||
import { Dropdown } from './generics/Dropdown'
|
||||
import { Book } from './Book'
|
||||
|
||||
import styles from './SearchBar.css'
|
||||
import { CursorList } from './generics/CursorList'
|
||||
|
||||
import { setText, setLang } from '../store/actions.js'
|
||||
|
||||
export const SearchBar = () => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isFocused, setFocus] = useState(false)
|
||||
|
||||
const debouncedSearch = useCallback(
|
||||
debounce(async term => {
|
||||
await search(term, 100).then(setResults)
|
||||
setLoading(false)
|
||||
}, 500),
|
||||
[]
|
||||
)
|
||||
|
||||
const handleClick = async idx => {
|
||||
const { id, language } = results[idx]
|
||||
const url = `https://gutenberg.muperfredi.de/texts/${id}/stripped-body`
|
||||
const text = await axios.get(url).then(res => res.data.body)
|
||||
dispatch(setText(text))
|
||||
dispatch(setLang(language[0]))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm.length > 0) {
|
||||
setLoading(true)
|
||||
debouncedSearch(searchTerm)
|
||||
} else {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [searchTerm, debouncedSearch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className={styles.label}>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
className={classNames({
|
||||
[styles.loading]: false,
|
||||
[styles.search]: true
|
||||
})}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
placeholder={t('search') + '...'}
|
||||
></input>
|
||||
{loading ? <Spinner /> : <FiSearch />}
|
||||
</label>
|
||||
<Dropdown visible={isFocused && searchTerm.length !== 0}>
|
||||
<CursorList
|
||||
items={results.map(entry => (
|
||||
<Book {...entry} />
|
||||
))}
|
||||
onSelect={handleClick}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
12
src/components/generics/CursorList.css
Normal file
12
src/components/generics/CursorList.css
Normal file
@ -0,0 +1,12 @@
|
||||
.item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectedItem {
|
||||
composes: item;
|
||||
background: #352e2e3b;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background: #3831313b;
|
||||
}
|
65
src/components/generics/CursorList.js
Normal file
65
src/components/generics/CursorList.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
|
||||
import { useKeyPress } from './useKeyPress'
|
||||
|
||||
import styles from './CursorList.css'
|
||||
|
||||
export const CursorList = ({ items, onSelect = noop }) => {
|
||||
const downPress = useKeyPress('ArrowDown')
|
||||
const upPress = useKeyPress('ArrowUp')
|
||||
const enterPress = useKeyPress('Enter')
|
||||
const [cursor, setCursor] = useState(0)
|
||||
const [hovered, setHovered] = useState(null)
|
||||
const selectedRef = useRef(null)
|
||||
|
||||
// react on keypresses
|
||||
useEffect(() => {
|
||||
const safeIncrement = cur => (cur < items.length - 1 ? cur + 1 : cur)
|
||||
const safeDecrement = cur => (cur > 0 ? cur - 1 : cur)
|
||||
if (downPress) setCursor(safeIncrement)
|
||||
if (upPress) setCursor(safeDecrement)
|
||||
}, [downPress, upPress, items])
|
||||
|
||||
// select item on enter
|
||||
useEffect(() => {
|
||||
if (items.length && enterPress) {
|
||||
onSelect(cursor)
|
||||
}
|
||||
}, [items, cursor, enterPress, onSelect])
|
||||
|
||||
// sync curser with hovered
|
||||
useEffect(() => {
|
||||
if (items.length && hovered !== null) {
|
||||
setCursor(hovered)
|
||||
}
|
||||
}, [items, hovered])
|
||||
|
||||
// scroll current into view
|
||||
useEffect(() => {
|
||||
if (selectedRef.current) {
|
||||
selectedRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest'
|
||||
})
|
||||
}
|
||||
}, [cursor])
|
||||
|
||||
return (
|
||||
<>
|
||||
{items.map((item, idx) => (
|
||||
<div
|
||||
className={idx === cursor ? styles.selectedItem : styles.item}
|
||||
key={idx}
|
||||
onClick={() => onSelect(idx)}
|
||||
onMouseEnter={() => setHovered(idx)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
ref={idx === cursor ? selectedRef : null}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const noop = () => {}
|
13
src/components/generics/Dropdown.css
Normal file
13
src/components/generics/Dropdown.css
Normal file
@ -0,0 +1,13 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
position: absolute;
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
16
src/components/generics/Dropdown.js
Normal file
16
src/components/generics/Dropdown.js
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './Dropdown.css'
|
||||
|
||||
export const Dropdown = ({ visible = true, children }) => {
|
||||
return (
|
||||
<div className={styles.dropdown}>
|
||||
<div
|
||||
className={styles.dropdownContent}
|
||||
style={{ display: visible ? 'block' : 'none', maxHeight: '500px' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
36
src/components/generics/useKeyPress.js
Normal file
36
src/components/generics/useKeyPress.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
|
||||
export const useKeyPress = targetKey => {
|
||||
const [keyPressed, setKeyPressed] = useState(false)
|
||||
|
||||
const downHandler = useCallback(
|
||||
({ key }) => {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(false)
|
||||
setKeyPressed(true)
|
||||
}
|
||||
},
|
||||
[targetKey]
|
||||
)
|
||||
|
||||
const upHandler = useCallback(
|
||||
({ key }) => {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(false)
|
||||
}
|
||||
},
|
||||
[targetKey]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler)
|
||||
window.addEventListener('keyup', upHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
window.removeEventListener('keyup', upHandler)
|
||||
}
|
||||
}, [downHandler, upHandler])
|
||||
|
||||
return keyPressed
|
||||
}
|
Loading…
Reference in New Issue
Block a user