Compare commits
No commits in common. "e50a929d8b9bc95f375fe2bee9ffe87bc36e2cc4" and "dc35932027444c96dce8db1f3269781d77f49c45" have entirely different histories.
e50a929d8b
...
dc35932027
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": ["react-app", "prettier"],
|
"extends": ["react-app", "prettier"],
|
||||||
"rules": { "no-console": ["warn", "allow": ["warn", "error"]] }
|
"rules": { "no-console": "warn" }
|
||||||
}
|
}
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
tmp/
|
tmp/
|
||||||
books/
|
|
@ -1,13 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
stories: ['../src/components/**/*.stories.js'],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-knobs/register',
|
|
||||||
'@storybook/addon-actions',
|
|
||||||
'@storybook/addon-links'
|
|
||||||
],
|
|
||||||
webpackFinal: async config => {
|
|
||||||
// do mutation to the config
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { addDecorator } from '@storybook/react'
|
|
||||||
import { withInfo } from '@storybook/addon-info'
|
|
||||||
import { withKnobs } from '@storybook/addon-knobs'
|
|
||||||
import { withConsole } from '@storybook/addon-console'
|
|
||||||
|
|
||||||
addDecorator(withInfo({ inline: true }))
|
|
||||||
addDecorator(withKnobs)
|
|
||||||
addDecorator((storyFn, context) => withConsole()(storyFn)(context))
|
|
@ -1,23 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'style-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
modules: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'postcss-loader'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,10 +2,7 @@ module.exports = api => {
|
|||||||
// caching the babel config
|
// caching the babel config
|
||||||
api.cache.using(() => process.env.NODE_ENV)
|
api.cache.using(() => process.env.NODE_ENV)
|
||||||
return {
|
return {
|
||||||
presets: [
|
presets: ['@babel/preset-env', '@babel/preset-react']
|
||||||
require.resolve('@babel/preset-env'),
|
|
||||||
require.resolve('@babel/preset-react')
|
|
||||||
]
|
|
||||||
// plugins: [api.env('development') && 'react-refresh/babel'].filter(Boolean)
|
// plugins: [api.env('development') && 'react-refresh/babel'].filter(Boolean)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "src"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
9314
package-lock.json
generated
9314
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
@ -10,68 +10,56 @@
|
|||||||
"serve": "ws -d build --compress --hostname 0.0.0.0",
|
"serve": "ws -d build --compress --hostname 0.0.0.0",
|
||||||
"test": "tests",
|
"test": "tests",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"format": "prettier --write src/**/*.js",
|
"format": "prettier --write src/**/*.js"
|
||||||
"graph": "mkdir -p build && madge src --exclude '.css$' --image build/dep-graph.svg",
|
|
||||||
"storybook": "start-storybook -p 6006 --ci",
|
|
||||||
"build-storybook": "build-storybook"
|
|
||||||
},
|
},
|
||||||
"author": "Alfred Melch (dev@melch.pro)",
|
"author": "Alfred Melch (dev@melch.pro)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"debounce": "^1.2.0",
|
"debounce": "^1.2.0",
|
||||||
"i18next": "^19.3.2",
|
"i18next": "^19.1.0",
|
||||||
"potent-reducer": "^0.1.0",
|
"react": "^16.12.0",
|
||||||
"react": "^16.13.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-i18next": "^11.3.1",
|
||||||
"react-i18next": "^11.3.3",
|
"react-icons": "^3.8.0",
|
||||||
"react-icons": "^3.9.0",
|
"react-redux": "^7.1.3",
|
||||||
"regenerator-runtime": "^0.13.5",
|
"redux": "^4.0.4",
|
||||||
|
"regenerator-runtime": "^0.13.3",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"reselect-tools": "0.0.7"
|
"reselect-tools": "0.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.7",
|
"@babel/core": "^7.7.5",
|
||||||
"@babel/preset-env": "^7.8.7",
|
"@babel/preset-env": "^7.7.6",
|
||||||
"@babel/preset-react": "^7.8.3",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.1",
|
||||||
"@storybook/addon-actions": "^5.3.14",
|
"babel-eslint": "^10.0.3",
|
||||||
"@storybook/addon-console": "^1.2.1",
|
|
||||||
"@storybook/addon-info": "^5.3.14",
|
|
||||||
"@storybook/addon-knobs": "^5.3.14",
|
|
||||||
"@storybook/addon-links": "^5.3.14",
|
|
||||||
"@storybook/addons": "^5.3.14",
|
|
||||||
"@storybook/react": "^5.3.14",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"css-loader": "^3.3.2",
|
||||||
"css-loader": "^3.4.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint": "^6.8.0",
|
"eslint-config-prettier": "^6.7.0",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-react-app": "^5.1.0",
|
||||||
"eslint-config-react-app": "^5.2.0",
|
"eslint-plugin-flowtype": "^4.5.2",
|
||||||
"eslint-plugin-flowtype": "^4.6.0",
|
"eslint-plugin-import": "^2.19.1",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
"eslint-plugin-react": "^7.19.0",
|
"eslint-plugin-react": "^7.17.0",
|
||||||
"eslint-plugin-react-hooks": "^2.5.0",
|
"eslint-plugin-react-hooks": "^2.3.0",
|
||||||
"html-webpack-plugin": "^4.0.0-beta.11",
|
"html-webpack-plugin": "^4.0.0-beta.11",
|
||||||
"local-web-server": "^4.0.0",
|
"local-web-server": "^3.0.7",
|
||||||
"madge": "^3.8.0",
|
"mini-css-extract-plugin": "^0.8.0",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
"postcss-flexbugs-fixes": "^4.2.0",
|
"postcss-flexbugs-fixes": "^4.1.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-normalize": "^8.0.1",
|
"postcss-normalize": "^8.0.1",
|
||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"prop-types": "^15.7.2",
|
"react-refresh": "^0.7.0",
|
||||||
"react-refresh": "^0.8.0",
|
"style-loader": "^1.0.1",
|
||||||
"style-loader": "^1.1.3",
|
"webpack": "^4.41.2",
|
||||||
"webpack": "^4.42.0",
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-bundle-analyzer": "^3.6.1",
|
"webpack-cli": "^3.3.10",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-dev-server": "^3.9.0",
|
||||||
"webpack-dev-server": "^3.10.3",
|
|
||||||
"webpackbar": "^4.0.0"
|
"webpackbar": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const booksDir = path.join(__dirname, '..', 'books')
|
|
||||||
|
|
||||||
function zip(...arrs) {
|
|
||||||
const resultLength = Math.min(...arrs.map(a => a.length))
|
|
||||||
return new Array(resultLength).fill(0).map((_, i) => arrs.map(a => a[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const scoresPage = await axios
|
|
||||||
.get('https://www.gutenberg.org/browse/scores/top')
|
|
||||||
.then(res => res.data)
|
|
||||||
const top100listItems = scoresPage.match(/<li>.*<\/li>/g).slice(0, 100)
|
|
||||||
|
|
||||||
const ids = []
|
|
||||||
const titles = []
|
|
||||||
|
|
||||||
for (let listItem of top100listItems) {
|
|
||||||
const [, id, title] = listItem.match(/<a href="\/ebooks\/(.*?)">(.*?)<\/a>/)
|
|
||||||
ids.push(id)
|
|
||||||
titles.push(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let id of ids) {
|
|
||||||
fetchBook(id)
|
|
||||||
.then(text => writeBookToFile(id, text))
|
|
||||||
.catch(err => {
|
|
||||||
console.warn(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(booksDir)
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
const indexFilePath = path.join(booksDir, 'index.json')
|
|
||||||
const indexObj = Object.fromEntries(zip(ids, titles))
|
|
||||||
fs.writeFileSync(indexFilePath, JSON.stringify(indexObj, undefined, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchBook(id) {
|
|
||||||
try {
|
|
||||||
return await axios
|
|
||||||
.get(`https://www.gutenberg.org/files/${id}/${id}-0.txt`)
|
|
||||||
.then(res => res.data)
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
return await axios(`https://www.gutenberg.org/ebooks/${id}.txt.utf-8`).then(
|
|
||||||
res => res.data
|
|
||||||
)
|
|
||||||
} catch {}
|
|
||||||
throw Error(`Could not fetch book with id ${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeBookToFile(id, text) {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(booksDir)
|
|
||||||
} catch {}
|
|
||||||
const filepath = path.join(booksDir, `${id}.txt`)
|
|
||||||
fs.writeFileSync(filepath, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
17
src/App.css
17
src/App.css
@ -4,7 +4,7 @@ html {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
background-color: #f8f8f8;
|
background-color: #fce6cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@ -12,10 +12,19 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: antiquewhite;
|
border-bottom: 2px solid white;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-color: antiquewhite;
|
border-top: 2px solid white;
|
||||||
margin-top: 2em;
|
margin-top: 1em;
|
||||||
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
47
src/App.js
47
src/App.js
@ -1,60 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { RsvpReader } from './components/RsvpReader'
|
||||||
import { SearchBar } from './components/SearchBar'
|
import { SearchBar } from './components/SearchBar'
|
||||||
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { LangSelect } from './components/LangSelect'
|
import { LangSelect } from './components/LangSelect'
|
||||||
|
|
||||||
import { Container } from './styles/Container'
|
|
||||||
import { Flex, FlexMain } from './styles/Flex'
|
|
||||||
import { RsvpWidget } from 'components/RsvpWidget'
|
|
||||||
import { Options } from 'components/Options'
|
|
||||||
import { TextInput } from 'components/TextInput'
|
|
||||||
import { TextOutput } from 'components/TextOutput'
|
|
||||||
import { TotalTime } from 'components/TotalTime'
|
|
||||||
import { Div } from 'styles/Div'
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
<Container>
|
<h1>{t('title')}</h1>
|
||||||
<Flex alignItems="center" flexWrap="wrap">
|
<SearchBar></SearchBar>
|
||||||
<FlexMain>
|
<LangSelect />
|
||||||
<h1>{t('title')}</h1>
|
|
||||||
</FlexMain>
|
|
||||||
<SearchBar></SearchBar>
|
|
||||||
<LangSelect />
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<Container>
|
<RsvpReader></RsvpReader>
|
||||||
<h2>Widget</h2>
|
|
||||||
<Div maxWidth="600px" margin="0 auto">
|
|
||||||
<RsvpWidget />
|
|
||||||
</Div>
|
|
||||||
<Flex flexWrap="wrap">
|
|
||||||
<FlexMain padding="10px" minWidth="300px">
|
|
||||||
<h2>Options</h2>
|
|
||||||
<Options />
|
|
||||||
</FlexMain>
|
|
||||||
<FlexMain padding="10px" minWidth="300px">
|
|
||||||
<h2>Text input</h2>
|
|
||||||
<TextInput />
|
|
||||||
</FlexMain>
|
|
||||||
</Flex>
|
|
||||||
<h2>Text Output</h2>
|
|
||||||
<TextOutput />
|
|
||||||
<h2>Time needed</h2>
|
|
||||||
<TotalTime />
|
|
||||||
</Container>
|
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>Made by Alfred Melch</footer>
|
||||||
<Container>Made by Alfred Melch</Container>
|
|
||||||
</footer>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
import React from 'react'
|
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'
|
import styles from './Book.css'
|
||||||
|
|
||||||
export const Book = ({ author, language, title }) => {
|
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 (
|
return (
|
||||||
<div className={styles.book}>
|
<div className={styles.book} onClick={handleClick}>
|
||||||
<div className={styles.title}>{title.join(' – ')}</div>
|
<div className={styles.title}>{title.join(' – ')}</div>
|
||||||
<div className={styles.author}>{author[0]}</div>
|
<div className={styles.author}>{author[0]}</div>
|
||||||
<div className={styles.language}>{language.join(' – ')}</div>
|
<div className={styles.language}>{language.join(' – ')}</div>
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import styles from './Indicator.css'
|
|
||||||
|
|
||||||
export const Indicator = ({ type = 'border', children }) => {
|
|
||||||
const IndicatorType = indicatorByType(type)
|
|
||||||
return (
|
|
||||||
<div className={styles.indicator}>
|
|
||||||
<IndicatorType>{children}</IndicatorType>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const indicatorByType = type => {
|
|
||||||
switch (type) {
|
|
||||||
case 'border':
|
|
||||||
return BorderIndicator
|
|
||||||
case 'pipe':
|
|
||||||
return PipeIndicator
|
|
||||||
default:
|
|
||||||
throw Error(`Indicator of type ${type} is not defined`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BorderIndicator = ({ children }) => (
|
|
||||||
<>
|
|
||||||
<div className={styles.border}></div>
|
|
||||||
<div className={styles.marker}></div>
|
|
||||||
{children}
|
|
||||||
<div className={styles.marker}></div>
|
|
||||||
<div className={styles.border}></div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const PipeIndicator = ({ children }) => (
|
|
||||||
<>
|
|
||||||
<div>|</div>
|
|
||||||
{children}
|
|
||||||
<div>|</div>
|
|
||||||
</>
|
|
||||||
)
|
|
@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Indicator } from './Indicator'
|
|
||||||
import { Segment } from './Segment'
|
|
||||||
import { radios } from '@storybook/addon-knobs'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: Indicator,
|
|
||||||
title: 'Indicator'
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeKnob = () =>
|
|
||||||
radios('Type', { Border: 'border', Pipe: 'pipe' }, 'border')
|
|
||||||
|
|
||||||
export const HappyPath = () => {
|
|
||||||
return <Indicator type={typeKnob()}>Hello World</Indicator>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WithSegment = () => {
|
|
||||||
return (
|
|
||||||
<Indicator type={typeKnob()}>
|
|
||||||
<Segment>Hello World</Segment>
|
|
||||||
</Indicator>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { useSelector } from 'store'
|
|
||||||
import { selectOffset } from 'store/selectors'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper that is of double width while hiding its overflow.
|
|
||||||
* Place children with offset (from -50 to 50, 0 is center)
|
|
||||||
*/
|
|
||||||
export const Offset = ({ offset = 0, children }) => {
|
|
||||||
offset = (offset - 50) / 2
|
|
||||||
return (
|
|
||||||
<div style={{ overflow: 'hidden' }}>
|
|
||||||
<div style={{ width: '200%' }}>
|
|
||||||
<div style={{ position: 'relative', left: `${offset}%` }}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Offset.propTypes = {
|
|
||||||
offset: PropTypes.number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RsvpOffset = ({ children }) => {
|
|
||||||
const offset = useSelector(selectOffset)
|
|
||||||
return <Offset offset={offset}>{children}</Offset>
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { number } from '@storybook/addon-knobs'
|
|
||||||
|
|
||||||
import { Offset } from './Offset'
|
|
||||||
import { Indicator } from './Indicator'
|
|
||||||
import { Segment } from './Segment'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: Offset,
|
|
||||||
title: 'Offset'
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetKnob = () =>
|
|
||||||
number('Offset', 0, {
|
|
||||||
range: true,
|
|
||||||
min: -50,
|
|
||||||
max: 50,
|
|
||||||
step: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
export const BlockElement = () => (
|
|
||||||
<Offset offset={offsetKnob()}>
|
|
||||||
<div style={{ textAlign: 'center' }}>This text is centered</div>
|
|
||||||
</Offset>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const InlineElement = () => (
|
|
||||||
<Offset offset={offsetKnob()}>
|
|
||||||
This a long text to see that inline elements get pushed far to the left. To
|
|
||||||
be precise they get pushed half the container with to the left. Use
|
|
||||||
text-align: center for better display.
|
|
||||||
</Offset>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const WithIndicator = () => (
|
|
||||||
<Offset offset={offsetKnob()}>
|
|
||||||
<Indicator>
|
|
||||||
<Segment>Hello World</Segment>
|
|
||||||
</Indicator>
|
|
||||||
</Offset>
|
|
||||||
)
|
|
@ -1,45 +1,49 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import { debounce } from 'debounce'
|
||||||
|
|
||||||
|
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 { useSelector, useDispatch } from '../store'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const availableLanguages = ['en', 'de']
|
const availableLanguages = ['en', 'de']
|
||||||
|
|
||||||
export const Options = () => {
|
export const Options = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
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)
|
||||||
const offset = useSelector(selectOffset)
|
const offset = useSelector(selectOffset)
|
||||||
const lang = useSelector(selectLang)
|
const lang = useSelector(selectLang)
|
||||||
const { setMaxLength, setWpm, setOffset, setLang } = useDispatch()
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<h2>{t('options.title')}</h2>
|
||||||
<Slider
|
<Slider
|
||||||
title={t('options.maxLength')}
|
title={t('options.maxLength')}
|
||||||
min={3}
|
min={3}
|
||||||
max={15}
|
max={15}
|
||||||
value={maxLength}
|
value={maxLength}
|
||||||
onChange={val => setMaxLength(val)}
|
onChange={debounce(val => dispatch(setMaxLength(val)), 100)}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
title={t('options.wpm')}
|
title={t('options.wpm')}
|
||||||
min={100}
|
min={100}
|
||||||
max={1000}
|
max={1000}
|
||||||
value={wpm}
|
value={wpm}
|
||||||
onChange={val => setWpm(val)}
|
onChange={debounce(val => dispatch(setWpm(val)), 50)}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
title={t('options.offset')}
|
title={t('options.offset')}
|
||||||
min={-50}
|
min={-50}
|
||||||
max={50}
|
max={50}
|
||||||
value={offset}
|
value={offset}
|
||||||
onChange={val => setOffset(val)}
|
onChange={val => dispatch(setOffset(val))}
|
||||||
/>
|
/>
|
||||||
<label>
|
<label>
|
||||||
Language:
|
Language:
|
||||||
<select value={lang} onChange={e => setLang(e.target.value)}>
|
<select value={lang} onChange={e => dispatch(setLang(e.target.value))}>
|
||||||
{availableLanguages.map(l => (
|
{availableLanguages.map(l => (
|
||||||
<option key={l}>{l}</option>
|
<option key={l}>{l}</option>
|
||||||
))}
|
))}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.indicator {
|
.wrapper {
|
||||||
text-align: center;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
28
src/components/PivotMarker.js
Normal file
28
src/components/PivotMarker.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
import styles from './PivotMarker.css'
|
||||||
|
import { selectOffset } from '../store/selectors'
|
||||||
|
|
||||||
|
export const PipeMarker = ({ children }) => (
|
||||||
|
<div>
|
||||||
|
<div style={{ textAlign: 'center' }}>|</div>
|
||||||
|
{children}
|
||||||
|
<div style={{ textAlign: 'center' }}>|</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const BorderMarker = ({ children }) => {
|
||||||
|
const offset = useSelector(selectOffset)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.border}></div>
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.marker} style={{ left: offset + '%' }}></div>
|
||||||
|
{children}
|
||||||
|
<div className={styles.marker} style={{ left: offset + '%' }}></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.border}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,37 +1,36 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { useDispatch, useSelector } from '../store'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectHasNextSegment,
|
selectHasNextSegment,
|
||||||
selectRunning,
|
selectRunning,
|
||||||
selectInterval
|
selectInterval
|
||||||
} from '../store/selectors'
|
} from '../store/selectors'
|
||||||
|
import { incrementSegment, stop, pause, start } from '../store/actions'
|
||||||
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
|
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
|
||||||
import { useInterval } from './generics/useInterval'
|
import { useInterval } from './generics/useInterval'
|
||||||
import { IconButton } from '../styles/IconButton'
|
import { IconButton } from '../styles/IconButton'
|
||||||
|
|
||||||
export const PlayerControl = () => {
|
export const PlayerControl = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
const running = useSelector(selectRunning)
|
const running = useSelector(selectRunning)
|
||||||
const hasNext = useSelector(selectHasNextSegment)
|
const hasNext = useSelector(selectHasNextSegment)
|
||||||
const interval = useSelector(selectInterval)
|
const interval = useSelector(selectInterval)
|
||||||
|
|
||||||
const { pause, start, stop, incSegment } = useDispatch()
|
|
||||||
|
|
||||||
useInterval(
|
useInterval(
|
||||||
() => {
|
() => {
|
||||||
if (!hasNext) pause()
|
if (!hasNext) dispatch(pause())
|
||||||
else incSegment()
|
else dispatch(incrementSegment())
|
||||||
},
|
},
|
||||||
running ? interval : null
|
running ? interval : null
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton Icon={FiSquare} onClick={() => stop()} />
|
<IconButton Icon={FiSquare} onClick={() => dispatch(stop())} />
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={running ? FiPause : FiPlay}
|
Icon={running ? FiPause : FiPlay}
|
||||||
onClick={() => (running ? pause() : start())}
|
onClick={() => dispatch(running ? pause() : start())}
|
||||||
disabled={!hasNext}
|
disabled={!hasNext}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from '../store'
|
import { useSelector } from 'react-redux'
|
||||||
import { selectCurrentSegmentIndex, selectSegments } from '../store/selectors'
|
import { selectCurrentSegmentIndex, selectSegments } from '../store/selectors'
|
||||||
|
|
||||||
export const Progress = () => {
|
export const Progress = () => {
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { TextInput } from './TextInput'
|
import { TextInput } from './TextInput'
|
||||||
import { TextOutput } from './TextOutput'
|
import { TextOutput } from './TextOutput'
|
||||||
|
import { SegmentControl } from './SegmentControl'
|
||||||
|
import { Segment } from './Segment'
|
||||||
import { Options } from './Options'
|
import { Options } from './Options'
|
||||||
|
import { PlayerControl } from './PlayerControl'
|
||||||
|
|
||||||
import styles from './RsvpReader.css'
|
import styles from './RsvpReader.css'
|
||||||
|
import { BorderMarker } from './PivotMarker'
|
||||||
|
import { Progress } from './Progress'
|
||||||
import { TotalTime } from './TotalTime'
|
import { TotalTime } from './TotalTime'
|
||||||
import { RsvpWidget } from './RsvpWidget'
|
|
||||||
|
|
||||||
export const RsvpReader = () => {
|
export const RsvpReader = () => {
|
||||||
return (
|
return (
|
||||||
@ -14,7 +18,15 @@ export const RsvpReader = () => {
|
|||||||
<TextInput />
|
<TextInput />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.mainItem}>
|
<div className={styles.mainItem}>
|
||||||
<RsvpWidget />
|
<Progress />
|
||||||
|
<BorderMarker>
|
||||||
|
<Segment />
|
||||||
|
</BorderMarker>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<SegmentControl>
|
||||||
|
<PlayerControl />
|
||||||
|
</SegmentControl>
|
||||||
|
</div>
|
||||||
<Options></Options>
|
<Options></Options>
|
||||||
<TotalTime />
|
<TotalTime />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Progress } from './Progress'
|
|
||||||
import { RsvpOffset } from './Offset'
|
|
||||||
import { Indicator } from './Indicator'
|
|
||||||
import { RsvpSegment } from './Segment'
|
|
||||||
import { SegmentControl } from './SegmentControl'
|
|
||||||
import { PlayerControl } from './PlayerControl'
|
|
||||||
|
|
||||||
export const RsvpWidget = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Progress />
|
|
||||||
<RsvpOffset>
|
|
||||||
<Indicator>
|
|
||||||
<RsvpSegment />
|
|
||||||
</Indicator>
|
|
||||||
</RsvpOffset>
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<SegmentControl>
|
|
||||||
<PlayerControl />
|
|
||||||
</SegmentControl>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,11 +2,11 @@ import React, { useState, useEffect, useCallback } from 'react'
|
|||||||
import { debounce } from 'debounce'
|
import { debounce } from 'debounce'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { FiSearch } from 'react-icons/fi'
|
import { FiSearch } from 'react-icons/fi'
|
||||||
import { useDispatch } from '../store'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import { search, getBook } from '../lib/gutenberg'
|
import { search } from '../lib/gutenberg'
|
||||||
import { Spinner } from '../styles/Spinner'
|
import { Spinner } from '../styles/Spinner'
|
||||||
import { Dropdown } from './generics/Dropdown'
|
import { Dropdown } from './generics/Dropdown'
|
||||||
import { Book } from './Book'
|
import { Book } from './Book'
|
||||||
@ -14,15 +14,16 @@ import { Book } from './Book'
|
|||||||
import styles from './SearchBar.css'
|
import styles from './SearchBar.css'
|
||||||
import { CursorList } from './generics/CursorList'
|
import { CursorList } from './generics/CursorList'
|
||||||
|
|
||||||
|
import { setText, setLang } from '../store/actions.js'
|
||||||
|
|
||||||
export const SearchBar = () => {
|
export const SearchBar = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useDispatch()
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [results, setResults] = useState([])
|
const [results, setResults] = useState([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [isFocused, setFocus] = useState(false)
|
const [isFocused, setFocus] = useState(false)
|
||||||
|
|
||||||
const { setText, setLang } = useDispatch()
|
|
||||||
|
|
||||||
const debouncedSearch = useCallback(
|
const debouncedSearch = useCallback(
|
||||||
debounce(async term => {
|
debounce(async term => {
|
||||||
await search(term, 100).then(setResults)
|
await search(term, 100).then(setResults)
|
||||||
@ -31,10 +32,12 @@ export const SearchBar = () => {
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSelect = async idx => {
|
const handleClick = async idx => {
|
||||||
const { id, language } = results[idx]
|
const { id, language } = results[idx]
|
||||||
setText(await getBook(id))
|
const url = `https://gutenberg.muperfredi.de/texts/${id}/stripped-body`
|
||||||
setLang(language[0])
|
const text = await axios.get(url).then(res => res.data.body)
|
||||||
|
dispatch(setText(text))
|
||||||
|
dispatch(setLang(language[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -69,7 +72,7 @@ export const SearchBar = () => {
|
|||||||
items={results.map(entry => (
|
items={results.map(entry => (
|
||||||
<Book {...entry} />
|
<Book {...entry} />
|
||||||
))}
|
))}
|
||||||
onSelect={handleSelect}
|
onSelect={handleClick}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,5 +16,4 @@
|
|||||||
}
|
}
|
||||||
.suffix {
|
.suffix {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import { useSelector } from 'react-redux'
|
||||||
|
import { selectPivotizedSegment, selectOffset } from '../store/selectors'
|
||||||
|
|
||||||
import styles from './Segment.css'
|
import styles from './Segment.css'
|
||||||
import { pivotize } from '../lib/pivotize'
|
|
||||||
import { selectCurrentSegment } from 'store/selectors'
|
|
||||||
import { useSelector } from 'store'
|
|
||||||
|
|
||||||
export const Segment = ({ children = '' }) => {
|
export const Segment = () => {
|
||||||
const [prefix, pivot, suffix] = pivotize(children)
|
const [prefix, pivot, suffix] = useSelector(selectPivotizedSegment)
|
||||||
|
const offset = useSelector(selectOffset)
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.wrapper}>
|
||||||
<span className={styles.prefix}>{prefix}</span>
|
<div className={styles.container} style={{ left: offset + '%' }}>
|
||||||
<span className={styles.pivot}>{pivot}</span>
|
<span className={styles.prefix}>{prefix}</span>
|
||||||
<span className={styles.suffix}>{suffix}</span>
|
<span className={styles.pivot}>{pivot}</span>
|
||||||
|
<span className={styles.suffix}>{suffix}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Segment.propTypes = {
|
|
||||||
children: PropTypes.string.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RsvpSegment = () => {
|
|
||||||
const segment = useSelector(selectCurrentSegment)
|
|
||||||
return <Segment>{segment}</Segment>
|
|
||||||
}
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { text } from '@storybook/addon-knobs'
|
|
||||||
|
|
||||||
import { Segment } from './Segment'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: Segment,
|
|
||||||
title: 'Segment'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HappyPath = () => {
|
|
||||||
return <Segment>{text('Text', 'Hello')}</Segment>
|
|
||||||
}
|
|
@ -1,11 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import {
|
import {
|
||||||
FiSkipBack,
|
FiSkipBack,
|
||||||
FiSkipForward,
|
FiSkipForward,
|
||||||
FiRewind,
|
FiRewind,
|
||||||
FiFastForward
|
FiFastForward
|
||||||
} from 'react-icons/fi'
|
} from 'react-icons/fi'
|
||||||
import { useDispatch, useSelector } from '../store'
|
|
||||||
import { IconButton } from '../styles/IconButton'
|
import { IconButton } from '../styles/IconButton'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -15,39 +16,45 @@ import {
|
|||||||
selectHasNextWord
|
selectHasNextWord
|
||||||
} from '../store/selectors'
|
} from '../store/selectors'
|
||||||
|
|
||||||
|
import {
|
||||||
|
incrementSentence,
|
||||||
|
incrementWord,
|
||||||
|
decrementSentence,
|
||||||
|
decrementWord
|
||||||
|
} from '../store/actions'
|
||||||
|
|
||||||
export const SegmentControl = ({ children }) => {
|
export const SegmentControl = ({ children }) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
const hasPrevSentence = useSelector(selectHasPrevSentence)
|
const hasPrevSentence = useSelector(selectHasPrevSentence)
|
||||||
const hasNextSentence = useSelector(selectHasNextSentence)
|
const hasNextSentence = useSelector(selectHasNextSentence)
|
||||||
const hasPrevWord = useSelector(selectHasPrevWord)
|
const hasPrevWord = useSelector(selectHasPrevWord)
|
||||||
const hasNextWord = useSelector(selectHasNextWord)
|
const hasNextWord = useSelector(selectHasNextWord)
|
||||||
|
|
||||||
const { decSentence, decWord } = useDispatch()
|
|
||||||
const { incSentence, incWord } = useDispatch()
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
title="Previous Sentence"
|
title="Previous Sentence"
|
||||||
onClick={() => decSentence()}
|
onClick={() => dispatch(decrementSentence())}
|
||||||
Icon={FiRewind}
|
Icon={FiRewind}
|
||||||
disabled={!hasPrevSentence}
|
disabled={!hasPrevSentence}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={FiSkipBack}
|
Icon={FiSkipBack}
|
||||||
title="Previous Word"
|
title="Previous Word"
|
||||||
onClick={() => decWord()}
|
onClick={() => dispatch(decrementWord())}
|
||||||
disabled={!hasPrevWord}
|
disabled={!hasPrevWord}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={FiSkipForward}
|
Icon={FiSkipForward}
|
||||||
title="Next Word"
|
title="Next Word"
|
||||||
onClick={() => incWord()}
|
onClick={() => dispatch(incrementWord())}
|
||||||
disabled={!hasNextWord}
|
disabled={!hasNextWord}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={FiFastForward}
|
Icon={FiFastForward}
|
||||||
title="Next Sentence"
|
title="Next Sentence"
|
||||||
onClick={() => incSentence()}
|
onClick={() => dispatch(incrementSentence())}
|
||||||
disabled={!hasNextSentence}
|
disabled={!hasNextSentence}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import styles from './Slider.css'
|
import styles from './Slider.css'
|
||||||
|
|
||||||
@ -9,15 +8,14 @@ export const Slider = ({
|
|||||||
value,
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
name,
|
name,
|
||||||
min = 0,
|
min,
|
||||||
max = 100,
|
max,
|
||||||
step = 1,
|
step,
|
||||||
onChange
|
onChange
|
||||||
}) => {
|
}) => {
|
||||||
value = value || defaultValue || (max - min) / 2 + min
|
|
||||||
// use ref for onChange to not trigger state updates on rerenders of parent
|
// use ref for onChange to not trigger state updates on rerenders of parent
|
||||||
const onChangeRef = useRef(null)
|
const onChangeRef = useRef(null)
|
||||||
const [internalValue, setInternalValue] = useState(value)
|
const [internalValue, setInternalValue] = useState(value || defaultValue)
|
||||||
|
|
||||||
// trigger external update on internal change
|
// trigger external update on internal change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -63,15 +61,3 @@ export const Slider = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const calcWidth = num => `${(String(num).length + 1) * 12}px`
|
const calcWidth = num => `${(String(num).length + 1) * 12}px`
|
||||||
|
|
||||||
Slider.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string,
|
|
||||||
value: PropTypes.number,
|
|
||||||
defaultValue: PropTypes.number,
|
|
||||||
name: PropTypes.string,
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
step: PropTypes.number,
|
|
||||||
onChange: PropTypes.func
|
|
||||||
}
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { action } from '@storybook/addon-actions'
|
|
||||||
|
|
||||||
import { Slider } from './Slider'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
component: Slider,
|
|
||||||
title: 'Slider'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Default = () => <Slider onChange={action()} />
|
|
||||||
|
|
||||||
export const CustomRange = () => <Slider onChange={action()} min={5} max={15} />
|
|
||||||
|
|
||||||
export const CustomStep = () => <Slider onChange={action()} step={5} />
|
|
@ -1,8 +1,7 @@
|
|||||||
.area {
|
.area {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 300px;
|
||||||
resize: vertical;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.load {
|
.load {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useDispatch } from '../store'
|
import { useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import { setText } from '../store/actions.js'
|
||||||
|
|
||||||
import styles from './TextInput.css'
|
import styles from './TextInput.css'
|
||||||
|
|
||||||
@ -8,7 +10,7 @@ const lorem =
|
|||||||
|
|
||||||
export const TextInput = () => {
|
export const TextInput = () => {
|
||||||
const [text, setTextState] = useState(lorem)
|
const [text, setTextState] = useState(lorem)
|
||||||
const { setText } = useDispatch()
|
const dispatch = useDispatch()
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<textarea
|
||||||
@ -16,7 +18,7 @@ export const TextInput = () => {
|
|||||||
defaultValue={text}
|
defaultValue={text}
|
||||||
onInput={e => setTextState(e.target.value)}
|
onInput={e => setTextState(e.target.value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
<button className={styles.load} onClick={() => setText(text)}>
|
<button className={styles.load} onClick={() => dispatch(setText(text))}>
|
||||||
Load
|
Load
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from '../store'
|
import { useSelector } from 'react-redux'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import { getNextSmallerNumber } from '../lib/array-util.js'
|
import { getNextSmallerNumber } from '../lib/array-util.js'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from '../store'
|
import { useSelector } from 'react-redux'
|
||||||
import { selectTotalTime } from '../store/selectors'
|
import { selectTotalTime } from '../store/selectors'
|
||||||
|
|
||||||
function formatTime(totalSeconds) {
|
function formatTime(totalSeconds) {
|
||||||
@ -12,5 +12,10 @@ function formatTime(totalSeconds) {
|
|||||||
|
|
||||||
export const TotalTime = () => {
|
export const TotalTime = () => {
|
||||||
const millis = useSelector(selectTotalTime)
|
const millis = useSelector(selectTotalTime)
|
||||||
return <span>Time needed for Text: {formatTime(millis / 1000)}</span>
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Time needed</h2>
|
||||||
|
Time needed for Text: {formatTime(millis / 1000)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ export const CursorList = ({ items, onSelect = noop }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedRef.current) {
|
if (selectedRef.current) {
|
||||||
selectedRef.current.scrollIntoView({
|
selectedRef.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
block: 'nearest'
|
block: 'nearest'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ import 'regenerator-runtime/runtime'
|
|||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { Provider } from './store'
|
import { Provider } from 'react-redux'
|
||||||
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
import { store } from './store/index.js'
|
||||||
|
|
||||||
import './i18n'
|
import './i18n'
|
||||||
|
|
||||||
@ -15,9 +16,8 @@ function createRootElement() {
|
|||||||
body.appendChild(root)
|
body.appendChild(root)
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
createRootElement()
|
createRootElement()
|
||||||
|
18
src/store/actions.js
Normal file
18
src/store/actions.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const setText = text => ({ type: 'SET_TEXT', payload: text })
|
||||||
|
export const resetSegment = () => ({ type: 'SET_CURRENT_SEGMENT', payload: 0 })
|
||||||
|
export const incrementSegment = () => ({ type: 'INC_SEGMENT' })
|
||||||
|
export const decrementSegment = () => ({ type: 'DEC_SEGMENT' })
|
||||||
|
export const incrementWord = () => ({ type: 'INC_WORD' })
|
||||||
|
export const decrementWord = () => ({ type: 'DEC_WORD' })
|
||||||
|
export const incrementSentence = () => ({ type: 'INC_SENTENCE' })
|
||||||
|
export const decrementSentence = () => ({ type: 'DEC_SENTENCE' })
|
||||||
|
export const setMaxLength = length => ({
|
||||||
|
type: 'SET_MAX_LENGTH',
|
||||||
|
payload: length
|
||||||
|
})
|
||||||
|
export const setWpm = wpm => ({ type: 'SET_WPM', payload: wpm })
|
||||||
|
export const setOffset = offset => ({ type: 'SET_OFFSET', payload: offset })
|
||||||
|
export const start = () => ({ type: 'START' })
|
||||||
|
export const stop = () => ({ type: 'STOP' })
|
||||||
|
export const pause = () => ({ type: 'PAUSE' })
|
||||||
|
export const setLang = lang => ({ type: 'SET_LANG', payload: lang })
|
@ -1,81 +1,14 @@
|
|||||||
import { createStore } from 'potent-reducer'
|
import { createStore } from 'redux'
|
||||||
import {
|
import { registerSelectors, getStateWith } from 'reselect-tools'
|
||||||
safeSelectNextWord,
|
|
||||||
safeSelectNextSentence,
|
|
||||||
safeSelectPrevWord,
|
|
||||||
safeSelectPrevSentence,
|
|
||||||
safeSelectNextSegment,
|
|
||||||
safeSelectPrevSegment
|
|
||||||
} from './selectors'
|
|
||||||
|
|
||||||
const initialState = {
|
import { reducerFn, initialState } from './reducer'
|
||||||
originalText: 'Sample Text',
|
import * as selectors from './selectors'
|
||||||
curIdx: 0,
|
|
||||||
maxLength: 10,
|
|
||||||
isPlaying: false,
|
|
||||||
wpm: 300,
|
|
||||||
offset: -15,
|
|
||||||
running: false,
|
|
||||||
lang: 'en',
|
|
||||||
textDisplay: {
|
|
||||||
mode: 'pagination',
|
|
||||||
options: {
|
|
||||||
pagination: { pageLength: 50 },
|
|
||||||
segments: { prev: 9, next: 40 },
|
|
||||||
sentences: { prev: 1, next: 8 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const thunks = {
|
export const store = createStore(
|
||||||
setText: text => ({ text }),
|
reducerFn,
|
||||||
resetSegment: () => {},
|
|
||||||
incSegment: () => {},
|
|
||||||
decSegment: () => {},
|
|
||||||
incWord: () => {},
|
|
||||||
decWord: () => {},
|
|
||||||
incSentence: () => {},
|
|
||||||
decSentence: () => {},
|
|
||||||
setMaxLength: maxLength => ({ maxLength }),
|
|
||||||
setWpm: wpm => ({ wpm }),
|
|
||||||
setOffset: offset => ({ offset }),
|
|
||||||
start: () => {},
|
|
||||||
stop: () => {},
|
|
||||||
pause: () => {},
|
|
||||||
setLang: lang => ({ lang })
|
|
||||||
}
|
|
||||||
|
|
||||||
const reducer = {
|
|
||||||
SET_TEXT: (state, { text }) => ({ ...state, originalText: text, curIdx: 0 }),
|
|
||||||
RESET_SEGMENT: state => ({ ...state, curIdx: 0 }),
|
|
||||||
INC_SEGMENT: state => ({ ...state, curIdx: safeSelectNextSegment(state) }),
|
|
||||||
DEC_SEGMENT: state => ({ ...state, curIdx: safeSelectPrevSegment(state) }),
|
|
||||||
INC_WORD: state => ({ ...state, curIdx: safeSelectNextWord(state) }),
|
|
||||||
DEC_WORD: state => ({ ...state, curIdx: safeSelectPrevWord(state) }),
|
|
||||||
INC_SENTENCE: state => ({ ...state, curIdx: safeSelectNextSentence(state) }),
|
|
||||||
DEC_SENTENCE: state => ({ ...state, curIdx: safeSelectPrevSentence(state) }),
|
|
||||||
SET_MAX_LENGTH: (state, { maxLength }) => ({
|
|
||||||
...state,
|
|
||||||
maxLength,
|
|
||||||
running: false,
|
|
||||||
curIdx: 0
|
|
||||||
}),
|
|
||||||
SET_WPM: (state, { wpm }) => ({ ...state, wpm }),
|
|
||||||
SET_OFFSET: (state, { offset }) => ({ ...state, offset }),
|
|
||||||
START: state => ({ ...state, running: true }),
|
|
||||||
STOP: state => ({ ...state, running: false, curIdx: 0 }),
|
|
||||||
PAUSE: state => ({ ...state, running: false }),
|
|
||||||
SET_LANG: (state, { lang }) => ({ ...state, lang })
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = createStore({
|
|
||||||
reducer,
|
|
||||||
thunks,
|
|
||||||
initialState,
|
initialState,
|
||||||
logging: false,
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
warnOnUndefinedSelect: true
|
)
|
||||||
})
|
|
||||||
export const Provider = store.Provider
|
registerSelectors(selectors)
|
||||||
export const useStore = store.useStore
|
getStateWith(() => store.getState())
|
||||||
export const useSelector = store.useSelector
|
|
||||||
export const useDispatch = store.useDispatch
|
|
||||||
|
58
src/store/reducer.js
Normal file
58
src/store/reducer.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
safeSelectNextWord,
|
||||||
|
safeSelectNextSentence,
|
||||||
|
safeSelectPrevWord,
|
||||||
|
safeSelectPrevSentence,
|
||||||
|
safeSelectNextSegment,
|
||||||
|
safeSelectPrevSegment
|
||||||
|
} from './selectors'
|
||||||
|
|
||||||
|
export const initialState = {
|
||||||
|
originalText: 'Sample Text',
|
||||||
|
curIdx: 0,
|
||||||
|
maxLength: 10,
|
||||||
|
isPlaying: false,
|
||||||
|
wpm: 300,
|
||||||
|
offset: -15,
|
||||||
|
running: false,
|
||||||
|
lang: 'en',
|
||||||
|
textDisplay: {
|
||||||
|
mode: 'pagination',
|
||||||
|
options: {
|
||||||
|
pagination: { pageLength: 50 },
|
||||||
|
segments: { prev: 9, next: 40 },
|
||||||
|
sentences: { prev: 1, next: 8 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducer = {
|
||||||
|
SET_TEXT: (state, payload) => ({
|
||||||
|
...state,
|
||||||
|
originalText: payload,
|
||||||
|
curIdx: 0
|
||||||
|
}),
|
||||||
|
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curIdx: payload }),
|
||||||
|
INC_SEGMENT: state => ({ ...state, curIdx: safeSelectNextSegment(state) }),
|
||||||
|
DEC_SEGMENT: state => ({ ...state, curIdx: safeSelectPrevSegment(state) }),
|
||||||
|
INC_WORD: state => ({ ...state, curIdx: safeSelectNextWord(state) }),
|
||||||
|
DEC_WORD: state => ({ ...state, curIdx: safeSelectPrevWord(state) }),
|
||||||
|
INC_SENTENCE: state => ({ ...state, curIdx: safeSelectNextSentence(state) }),
|
||||||
|
DEC_SENTENCE: state => ({ ...state, curIdx: safeSelectPrevSentence(state) }),
|
||||||
|
SET_MAX_LENGTH: (state, payload) => ({
|
||||||
|
...state,
|
||||||
|
maxLength: payload,
|
||||||
|
running: false,
|
||||||
|
curIdx: 0
|
||||||
|
}),
|
||||||
|
SET_WPM: (state, payload) => ({ ...state, wpm: payload }),
|
||||||
|
SET_OFFSET: (state, payload) => ({ ...state, offset: payload }),
|
||||||
|
START: state => ({ ...state, running: true }),
|
||||||
|
STOP: state => ({ ...state, running: false, curIdx: 0 }),
|
||||||
|
PAUSE: state => ({ ...state, running: false }),
|
||||||
|
SET_LANG: (state, payload) => ({ ...state, lang: payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducerFn = (state, { type, payload }) => {
|
||||||
|
return reducer[type] ? reducer[type](state, payload) : state
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
.container {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small devices (landscape phones, 576px and up) */
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Medium devices (tablets, 768px and up) */
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.container {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Large devices (desktops, 992px and up) */
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
.container {
|
|
||||||
width: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra large devices (large desktops, 1200px and up) */
|
|
||||||
@media (min-width: 1200px) {
|
|
||||||
.container {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import styles from './Container.css'
|
|
||||||
|
|
||||||
export const Container = ({ children }) => {
|
|
||||||
return <div className={styles.container}>{children}</div>
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
/** A generic div. Mainly for development. Try to use as sparse as possible */
|
|
||||||
export const Div = ({ children, ...rest }) => {
|
|
||||||
return <div style={rest}>{children}</div>
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mainItem {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import styles from './Flex.css'
|
|
||||||
|
|
||||||
export const Flex = ({ children, ...rest }) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.container} style={rest}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FlexMain = ({ children, ...rest }) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.mainItem} style={rest}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin')
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
||||||
@ -26,16 +25,16 @@ module.exports = (env, argv) => {
|
|||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: require.resolve('babel-loader')
|
loader: 'babel-loader'
|
||||||
},
|
},
|
||||||
// process css. css modules are enabled.
|
// process css. css modules are enabled.
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [
|
use: [
|
||||||
isEnvDevelopment && require.resolve('style-loader'),
|
isEnvDevelopment && 'style-loader',
|
||||||
isEnvProduction && MiniCssExtractPlugin.loader,
|
isEnvProduction && MiniCssExtractPlugin.loader,
|
||||||
{
|
{
|
||||||
loader: require.resolve('css-loader'),
|
loader: 'css-loader',
|
||||||
options: {
|
options: {
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: isEnvProduction
|
localIdentName: isEnvProduction
|
||||||
@ -44,7 +43,7 @@ module.exports = (env, argv) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
require.resolve('postcss-loader')
|
'postcss-loader'
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -59,9 +58,7 @@ module.exports = (env, argv) => {
|
|||||||
name: entrypoint => `runtime-${entrypoint.name}`
|
name: entrypoint => `runtime-${entrypoint.name}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: { modules: ['src', 'node_modules'] },
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyPlugin([{ from: 'books', to: 'books' }]),
|
|
||||||
// creat an index.html
|
// creat an index.html
|
||||||
new HtmlWebpackPlugin(),
|
new HtmlWebpackPlugin(),
|
||||||
// show a progress bar
|
// show a progress bar
|
||||||
|
Loading…
x
Reference in New Issue
Block a user