Compare commits
7 Commits
f28b5b1c50
...
ee62b85008
Author | SHA1 | Date | |
---|---|---|---|
ee62b85008 | |||
f9ec31fd7a | |||
e98ea6eb87 | |||
8ce324cd04 | |||
1d1b2a4dc0 | |||
495ba40f86 | |||
00c2440163 |
30
package-lock.json
generated
30
package-lock.json
generated
@ -5475,6 +5475,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"html-parse-stringify2": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
|
||||||
|
"requires": {
|
||||||
|
"void-elements": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"html-webpack-plugin": {
|
"html-webpack-plugin": {
|
||||||
"version": "4.0.0-beta.11",
|
"version": "4.0.0-beta.11",
|
||||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz",
|
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz",
|
||||||
@ -5650,6 +5658,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"i18next": {
|
||||||
|
"version": "19.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.1.0.tgz",
|
||||||
|
"integrity": "sha512-ISbmukX4L6Dz0QoH9+EW1AnBw7j+NRLoMu9uLPMaNSSTP9Eie9/oUL0dOyWX15baB3gYOpkHJpGZRHOqcnl0ew==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@ -9497,6 +9513,15 @@
|
|||||||
"integrity": "sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA==",
|
"integrity": "sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"react-i18next": {
|
||||||
|
"version": "11.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.3.1.tgz",
|
||||||
|
"integrity": "sha512-S/CWHcnew1lXo8HeniGhBU5kTmPhZ4w4rtA4m/gDN07soCtKKYSAcLNm7zhwjI2OSR4Skd0vOtzNp/FzEEjxIw==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.3.1",
|
||||||
|
"html-parse-stringify2": "2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-icons": {
|
"react-icons": {
|
||||||
"version": "3.8.0",
|
"version": "3.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.8.0.tgz",
|
||||||
@ -11548,6 +11573,11 @@
|
|||||||
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"void-elements": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
|
||||||
|
},
|
||||||
"walk-back": {
|
"walk-back": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz",
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.0",
|
||||||
|
"i18next": "^19.1.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-i18next": "^11.3.1",
|
||||||
"react-icons": "^3.8.0",
|
"react-icons": "^3.8.0",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
|
30
src/App.css
Normal file
30
src/App.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
background-color: #fce6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 2px solid white;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 2px solid white;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
12
src/App.js
12
src/App.js
@ -1,13 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { RsvpReader } from './components/RsvpReader'
|
import { RsvpReader } from './components/RsvpReader'
|
||||||
import { GutenbergSearch } from './components/GutenbergSearch'
|
import { GutenbergSearch } from './components/GutenbergSearch'
|
||||||
|
|
||||||
|
import './App.css'
|
||||||
|
import { LangSelect } from './components/LangSelect'
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<header>
|
||||||
|
<h1>{t('title')}</h1>
|
||||||
|
<LangSelect />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
<RsvpReader></RsvpReader>
|
<RsvpReader></RsvpReader>
|
||||||
<GutenbergSearch></GutenbergSearch>
|
<GutenbergSearch></GutenbergSearch>
|
||||||
|
</main>
|
||||||
|
<footer>Made by Alfred Melch</footer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -34,10 +34,10 @@ const Book = ({ entry }) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{id} {title.join(' - ')}
|
{id} {title.join(' – ')}
|
||||||
</div>
|
</div>
|
||||||
<div>{author[0]}</div>
|
<div>{author[0]}</div>
|
||||||
<div>{language.join(' - ')}</div>
|
<div>{language.join(' – ')}</div>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={handleClick}>Load</button>
|
<button onClick={handleClick}>Load</button>
|
||||||
</div>
|
</div>
|
||||||
|
22
src/components/LangSelect.js
Normal file
22
src/components/LangSelect.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { languages } from '../i18n'
|
||||||
|
|
||||||
|
export const LangSelect = () => {
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
value={i18n.language}
|
||||||
|
onChange={evt => i18n.changeLanguage(evt.target.value)}
|
||||||
|
>
|
||||||
|
{languages.map(lng => (
|
||||||
|
<option key={lng} value={lng}>
|
||||||
|
{lng}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -5,10 +5,12 @@ import { debounce } from 'debounce'
|
|||||||
import { setMaxLength, setWpm, setOffset, setLang } from '../store/actions'
|
import { setMaxLength, setWpm, setOffset, setLang } from '../store/actions'
|
||||||
import { Slider } from './Slider'
|
import { Slider } from './Slider'
|
||||||
import { selectOffset, selectLang } from '../store/selectors'
|
import { selectOffset, selectLang } from '../store/selectors'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const availableLanguages = ['en', 'de']
|
const availableLanguages = ['en', 'de']
|
||||||
|
|
||||||
export const Options = () => {
|
export const Options = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const maxLength = useSelector(state => state.maxLength)
|
const maxLength = useSelector(state => state.maxLength)
|
||||||
const wpm = useSelector(state => state.wpm)
|
const wpm = useSelector(state => state.wpm)
|
||||||
@ -17,23 +19,23 @@ export const Options = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Options</h2>
|
<h2>{t('options.title')}</h2>
|
||||||
<Slider
|
<Slider
|
||||||
title={'Maximum segment length'}
|
title={t('options.maxLength')}
|
||||||
min={3}
|
min={3}
|
||||||
max={15}
|
max={15}
|
||||||
value={maxLength}
|
value={maxLength}
|
||||||
onChange={debounce(val => dispatch(setMaxLength(val)), 100)}
|
onChange={debounce(val => dispatch(setMaxLength(val)), 100)}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
title={'Words per minute'}
|
title={t('options.wpm')}
|
||||||
min={100}
|
min={100}
|
||||||
max={1000}
|
max={1000}
|
||||||
value={wpm}
|
value={wpm}
|
||||||
onChange={debounce(val => dispatch(setWpm(val)), 50)}
|
onChange={debounce(val => dispatch(setWpm(val)), 50)}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
title={'Offset from center'}
|
title={t('options.offset')}
|
||||||
min={-50}
|
min={-50}
|
||||||
max={50}
|
max={50}
|
||||||
value={offset}
|
value={offset}
|
||||||
|
9
src/components/TextInput.css
Normal file
9
src/components/TextInput.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.area {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -3,6 +3,8 @@ import { useDispatch } from 'react-redux'
|
|||||||
|
|
||||||
import { setText } from '../store/actions.js'
|
import { setText } from '../store/actions.js'
|
||||||
|
|
||||||
|
import styles from './TextInput.css'
|
||||||
|
|
||||||
const lorem =
|
const lorem =
|
||||||
'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.'
|
'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.'
|
||||||
|
|
||||||
@ -12,10 +14,13 @@ export const TextInput = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<textarea
|
||||||
|
className={styles.area}
|
||||||
defaultValue={text}
|
defaultValue={text}
|
||||||
onInput={e => setTextState(e.target.value)}
|
onInput={e => setTextState(e.target.value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
<button onClick={() => dispatch(setText(text))}>Load</button>
|
<button className={styles.load} onClick={() => dispatch(setText(text))}>
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
10
src/i18n/de.json
Normal file
10
src/i18n/de.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "Der schnelle leser",
|
||||||
|
"search": "Suche",
|
||||||
|
"options": {
|
||||||
|
"title": "Optionen",
|
||||||
|
"maxLength": "Maximale Segmentlänge",
|
||||||
|
"wpm": "Wörter pro Minute",
|
||||||
|
"offset": "Versatz der Wortanzeige"
|
||||||
|
}
|
||||||
|
}
|
10
src/i18n/en.json
Normal file
10
src/i18n/en.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "The Fast Reader",
|
||||||
|
"search": "Search",
|
||||||
|
"options": {
|
||||||
|
"title": "Options",
|
||||||
|
"maxLength": "Maximum segment length",
|
||||||
|
"wpm": "Words per minute",
|
||||||
|
"offset": "Offset from center"
|
||||||
|
}
|
||||||
|
}
|
22
src/i18n/index.js
Normal file
22
src/i18n/index.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
|
||||||
|
import de from './de.json'
|
||||||
|
import en from './en.json'
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
en: { translation: en },
|
||||||
|
de: { translation: de }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(de, en)
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
resources,
|
||||||
|
lng: 'en',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const languages = Object.keys(resources)
|
@ -7,6 +7,8 @@ import { Provider } from 'react-redux'
|
|||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
import { store } from './store/index.js'
|
import { store } from './store/index.js'
|
||||||
|
|
||||||
|
import './i18n'
|
||||||
|
|
||||||
function createRootElement() {
|
function createRootElement() {
|
||||||
const body = document.getElementsByTagName('body')[0]
|
const body = document.getElementsByTagName('body')[0]
|
||||||
const root = document.createElement('div')
|
const root = document.createElement('div')
|
||||||
|
23
src/lib/gutenberg.js
Normal file
23
src/lib/gutenberg.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import Axios from 'axios'
|
||||||
|
|
||||||
|
export async function search(searchTerm, maxResults = Infinity) {
|
||||||
|
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 >= maxResults) break
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBook(bookId) {
|
||||||
|
const url = `https://gutenberg.muperfredi.de/texts/${bookId}/stripped-body`
|
||||||
|
const text = await Axios.get(url).then(res => res.data.body)
|
||||||
|
return text
|
||||||
|
}
|
12
src/styles/Spinner.css
Normal file
12
src/styles/Spinner.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
animation: rotation 2s linear infinite;
|
||||||
|
}
|
8
src/styles/Spinner.js
Normal file
8
src/styles/Spinner.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { FiLoader } from 'react-icons/fi'
|
||||||
|
|
||||||
|
import styles from './Spinner.css'
|
||||||
|
|
||||||
|
export const Spinner = () => {
|
||||||
|
return <FiLoader className={styles.spinner} />
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user