Compare commits

...

7 Commits

Author SHA1 Message Date
dc35932027 Add new dropdown search 2020-02-01 09:24:47 +01:00
e811b4896e Remove unused var 2020-02-01 09:24:38 +01:00
dece6e8c47 Remove old search 2020-02-01 09:24:31 +01:00
b9a5d7f1e8 warn about console logging 2020-02-01 09:23:51 +01:00
6b83912973 remove console logging 2020-02-01 09:23:39 +01:00
ac3700e5ff remove console logging 2020-02-01 09:21:06 +01:00
e34fd06172 serve: make externally available 2020-01-31 22:15:32 +01:00
16 changed files with 295 additions and 91 deletions

View File

@ -1,3 +1,4 @@
{
"extends": ["react-app", "prettier"]
"extends": ["react-app", "prettier"],
"rules": { "no-console": "warn" }
}

View File

@ -7,7 +7,7 @@
"start": "webpack-dev-server --mode development",
"build": "webpack --mode production",
"build-fresh": "rm -rf build/ && npm run build",
"serve": "ws -d build --compress",
"serve": "ws -d build --compress --hostname 0.0.0.0",
"test": "tests",
"lint": "eslint src",
"format": "prettier --write src/**/*.js"

View File

@ -2,7 +2,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
import { RsvpReader } from './components/RsvpReader'
import { GutenbergSearch } from './components/GutenbergSearch'
import { SearchBar } from './components/SearchBar'
import './App.css'
import { LangSelect } from './components/LangSelect'
@ -13,11 +13,11 @@ export const App = () => {
<>
<header>
<h1>{t('title')}</h1>
<SearchBar></SearchBar>
<LangSelect />
</header>
<main>
<RsvpReader></RsvpReader>
<GutenbergSearch></GutenbergSearch>
</main>
<footer>Made by Alfred Melch</footer>
</>

24
src/components/Book.css Normal file
View 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
View 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>
)
}

View File

@ -1,83 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import axios from 'axios'
import { useDispatch } from 'react-redux'
import { debounce } from 'debounce'
import { setText, setLang } from '../store/actions.js'
async function search(searchTerm) {
const regex = new RegExp(searchTerm, 'i')
const result = []
const data = await import('../../data/gutenberg.json').then(
module => module.default
)
for (let entry of data) {
if (regex.test(entry.title[0]) || regex.test(entry.author[0])) {
result.push(entry)
}
if (result.length >= 20) break
}
return result
}
const Book = ({ entry }) => {
const dispatch = useDispatch()
const { author, language, rights, subject, title, id } = entry
const handleClick = async () => {
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]))
}
return (
<div>
<div>
{id} {title.join(' ')}
</div>
<div>{author[0]}</div>
<div>{language.join(' ')}</div>
<div>
<button onClick={handleClick}>Load</button>
</div>
<br></br>
</div>
)
}
export const GutenbergSearch = () => {
const [searchTerm, setSearchTerm] = useState('')
const [result, setResult] = useState([])
const [loading, setLoading] = useState(false)
const debouncedSearch = useCallback(
debounce(async term => {
setLoading(true)
await search(term).then(setResult)
setLoading(false)
}, 500),
[]
)
useEffect(() => {
if (searchTerm.length > 0) {
debouncedSearch(searchTerm)
}
}, [searchTerm, debouncedSearch])
return (
<div>
<h2>Search for books in the Gutenberg Project</h2>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
></input>
{loading && 'loading...'}
{result.map(entry => (
<Book key={entry.id} entry={entry} />
))}
{result.length === 0 && <div>'no results to display'</div>}
</div>
)
}

View 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;
}

View 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>
</>
)
}

View File

@ -6,7 +6,6 @@ import { getNextSmallerNumber } from '../lib/array-util.js'
import {
selectParsedText,
selectCurrentSegmentIndex,
selectDisplayMode,
selectSegmentWindow
} from '../store/selectors.js'
@ -16,7 +15,6 @@ export const TextOutput = () => {
const { segments, offset } = useSelector(selectSegmentWindow)
const { sentences, words } = useSelector(selectParsedText)
const curSegment = useSelector(selectCurrentSegmentIndex)
const mode = useSelector(selectDisplayMode)
return (
<>
<div>

View File

@ -0,0 +1,12 @@
.item {
cursor: pointer;
}
.selectedItem {
composes: item;
background: #352e2e3b;
}
.item:hover {
background: #3831313b;
}

View 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 = () => {}

View 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;
}

View 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>
)
}

View File

@ -12,7 +12,6 @@ export const Player = ({ onTick, onStart, onStop, onPause, delay }) => {
const handleStart = () => {
safeCall(onStart)
start()
console.log('yay')
}
const handleStop = () => {

View 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
}

View File

@ -9,7 +9,6 @@ const resources = {
de: { translation: de }
}
console.log(de, en)
i18n.use(initReactI18next).init({
resources,
lng: 'en',