Compare commits

...

7 Commits

Author SHA1 Message Date
ee62b85008 Add i18next 2020-01-31 22:15:04 +01:00
f9ec31fd7a Add spinner element 2019-12-28 12:21:00 +01:00
e98ea6eb87 Add gutenberg utility functions 2019-12-28 12:18:38 +01:00
8ce324cd04 styling 2019-12-28 12:18:04 +01:00
1d1b2a4dc0 Add page styling, add header/footer 2019-12-27 11:45:10 +01:00
495ba40f86 Use em dash for title seperators 2019-12-22 20:02:34 +01:00
00c2440163 Style textinput 2019-12-22 19:57:07 +01:00
16 changed files with 208 additions and 9 deletions

30
package-lock.json generated
View File

@ -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": {
"version": "4.0.0-beta.11",
"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": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -9497,6 +9513,15 @@
"integrity": "sha512-ueZzLmHltszTshDMwyfELDq8zOA803wQ1ZuzCccXa1m57k1PxSHfflPD5W9YIiTXLs0JTLzoj6o1LuM5N6zzNA==",
"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": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.8.0.tgz",
@ -11548,6 +11573,11 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz",

View File

@ -18,8 +18,10 @@
"axios": "^0.19.0",
"classnames": "^2.2.6",
"debounce": "^1.2.0",
"i18next": "^19.1.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-i18next": "^11.3.1",
"react-icons": "^3.8.0",
"react-redux": "^7.1.3",
"redux": "^4.0.4",

30
src/App.css Normal file
View 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;
}

View File

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

View File

@ -34,10 +34,10 @@ const Book = ({ entry }) => {
return (
<div>
<div>
{id} {title.join(' - ')}
{id} {title.join(' ')}
</div>
<div>{author[0]}</div>
<div>{language.join(' - ')}</div>
<div>{language.join(' ')}</div>
<div>
<button onClick={handleClick}>Load</button>
</div>

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

View File

@ -5,10 +5,12 @@ import { debounce } from 'debounce'
import { setMaxLength, setWpm, setOffset, setLang } from '../store/actions'
import { Slider } from './Slider'
import { selectOffset, selectLang } from '../store/selectors'
import { useTranslation } from 'react-i18next'
const availableLanguages = ['en', 'de']
export const Options = () => {
const { t } = useTranslation()
const dispatch = useDispatch()
const maxLength = useSelector(state => state.maxLength)
const wpm = useSelector(state => state.wpm)
@ -17,23 +19,23 @@ export const Options = () => {
return (
<div>
<h2>Options</h2>
<h2>{t('options.title')}</h2>
<Slider
title={'Maximum segment length'}
title={t('options.maxLength')}
min={3}
max={15}
value={maxLength}
onChange={debounce(val => dispatch(setMaxLength(val)), 100)}
/>
<Slider
title={'Words per minute'}
title={t('options.wpm')}
min={100}
max={1000}
value={wpm}
onChange={debounce(val => dispatch(setWpm(val)), 50)}
/>
<Slider
title={'Offset from center'}
title={t('options.offset')}
min={-50}
max={50}
value={offset}

View File

@ -0,0 +1,9 @@
.area {
box-sizing: border-box;
width: 100%;
height: 300px;
}
.load {
width: 100%;
}

View File

@ -3,6 +3,8 @@ import { useDispatch } from 'react-redux'
import { setText } from '../store/actions.js'
import styles from './TextInput.css'
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.'
@ -12,10 +14,13 @@ export const TextInput = () => {
return (
<div>
<textarea
className={styles.area}
defaultValue={text}
onInput={e => setTextState(e.target.value)}
></textarea>
<button onClick={() => dispatch(setText(text))}>Load</button>
<button className={styles.load} onClick={() => dispatch(setText(text))}>
Load
</button>
</div>
)
}

10
src/i18n/de.json Normal file
View 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
View 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
View 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)

View File

@ -7,6 +7,8 @@ import { Provider } from 'react-redux'
import { App } from './App'
import { store } from './store/index.js'
import './i18n'
function createRootElement() {
const body = document.getElementsByTagName('body')[0]
const root = document.createElement('div')

23
src/lib/gutenberg.js Normal file
View 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
View 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
View 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} />
}