Compare commits
No commits in common. "dc35932027444c96dce8db1f3269781d77f49c45" and "ee62b850087d3e51eb19806a0d70aed8ef110be6" have entirely different histories.
dc35932027
...
ee62b85008
@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": ["react-app", "prettier"],
|
"extends": ["react-app", "prettier"]
|
||||||
"rules": { "no-console": "warn" }
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"start": "webpack-dev-server --mode development",
|
"start": "webpack-dev-server --mode development",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"build-fresh": "rm -rf build/ && npm run build",
|
"build-fresh": "rm -rf build/ && npm run build",
|
||||||
"serve": "ws -d build --compress --hostname 0.0.0.0",
|
"serve": "ws -d build --compress",
|
||||||
"test": "tests",
|
"test": "tests",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"format": "prettier --write src/**/*.js"
|
"format": "prettier --write src/**/*.js"
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { RsvpReader } from './components/RsvpReader'
|
import { RsvpReader } from './components/RsvpReader'
|
||||||
import { SearchBar } from './components/SearchBar'
|
import { GutenbergSearch } from './components/GutenbergSearch'
|
||||||
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { LangSelect } from './components/LangSelect'
|
import { LangSelect } from './components/LangSelect'
|
||||||
@ -13,11 +13,11 @@ export const App = () => {
|
|||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<h1>{t('title')}</h1>
|
<h1>{t('title')}</h1>
|
||||||
<SearchBar></SearchBar>
|
|
||||||
<LangSelect />
|
<LangSelect />
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<RsvpReader></RsvpReader>
|
<RsvpReader></RsvpReader>
|
||||||
|
<GutenbergSearch></GutenbergSearch>
|
||||||
</main>
|
</main>
|
||||||
<footer>Made by Alfred Melch</footer>
|
<footer>Made by Alfred Melch</footer>
|
||||||
</>
|
</>
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
83
src/components/GutenbergSearch.js
Normal file
83
src/components/GutenbergSearch.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ import { getNextSmallerNumber } from '../lib/array-util.js'
|
|||||||
import {
|
import {
|
||||||
selectParsedText,
|
selectParsedText,
|
||||||
selectCurrentSegmentIndex,
|
selectCurrentSegmentIndex,
|
||||||
|
selectDisplayMode,
|
||||||
selectSegmentWindow
|
selectSegmentWindow
|
||||||
} from '../store/selectors.js'
|
} from '../store/selectors.js'
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export const TextOutput = () => {
|
|||||||
const { segments, offset } = useSelector(selectSegmentWindow)
|
const { segments, offset } = useSelector(selectSegmentWindow)
|
||||||
const { sentences, words } = useSelector(selectParsedText)
|
const { sentences, words } = useSelector(selectParsedText)
|
||||||
const curSegment = useSelector(selectCurrentSegmentIndex)
|
const curSegment = useSelector(selectCurrentSegmentIndex)
|
||||||
|
const mode = useSelector(selectDisplayMode)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
.item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectedItem {
|
|
||||||
composes: item;
|
|
||||||
background: #352e2e3b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover {
|
|
||||||
background: #3831313b;
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
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 = () => {}
|
|
@ -1,13 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -12,6 +12,7 @@ export const Player = ({ onTick, onStart, onStop, onPause, delay }) => {
|
|||||||
const handleStart = () => {
|
const handleStart = () => {
|
||||||
safeCall(onStart)
|
safeCall(onStart)
|
||||||
start()
|
start()
|
||||||
|
console.log('yay')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStop = () => {
|
const handleStop = () => {
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ const resources = {
|
|||||||
de: { translation: de }
|
de: { translation: de }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(de, en)
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
resources,
|
resources,
|
||||||
lng: 'en',
|
lng: 'en',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user