Compare commits

...

6 Commits

17 changed files with 1030 additions and 1637 deletions

View File

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

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
node_modules/ node_modules/
build/ build/
tmp/ tmp/
books/

View File

@ -2,5 +2,5 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": "src" "baseUrl": "src"
}, },
"include": ["src", "lib"] "include": ["src"]
} }

2437
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,21 +21,21 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"debounce": "^1.2.0", "debounce": "^1.2.0",
"i18next": "^19.1.0", "i18next": "^19.3.2",
"potent-reducer": "^0.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.9.0", "react-icons": "^3.9.0",
"regenerator-runtime": "^0.13.3", "regenerator-runtime": "^0.13.5",
"reselect": "^4.0.0", "reselect": "^4.0.0",
"reselect-tools": "0.0.7" "reselect-tools": "0.0.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.8.4", "@babel/core": "^7.8.7",
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3", "@babel/preset-react": "^7.8.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.2.0",
"@storybook/addon-actions": "^5.3.14", "@storybook/addon-actions": "^5.3.14",
"@storybook/addon-console": "^1.2.1", "@storybook/addon-console": "^1.2.1",
"@storybook/addon-info": "^5.3.14", "@storybook/addon-info": "^5.3.14",
@ -43,8 +43,9 @@
"@storybook/addon-links": "^5.3.14", "@storybook/addon-links": "^5.3.14",
"@storybook/addons": "^5.3.14", "@storybook/addons": "^5.3.14",
"@storybook/react": "^5.3.14", "@storybook/react": "^5.3.14",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.1.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0", "eslint-config-prettier": "^6.10.0",
@ -52,25 +53,25 @@
"eslint-plugin-flowtype": "^4.6.0", "eslint-plugin-flowtype": "^4.6.0",
"eslint-plugin-import": "^2.20.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.18.2", "eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^2.3.0", "eslint-plugin-react-hooks": "^2.5.0",
"html-webpack-plugin": "^4.0.0-beta.11", "html-webpack-plugin": "^4.0.0-beta.11",
"local-web-server": "^3.0.7", "local-web-server": "^4.0.0",
"madge": "^3.7.0", "madge": "^3.8.0",
"mini-css-extract-plugin": "^0.9.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.1.0", "postcss-flexbugs-fixes": "^4.2.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", "prop-types": "^15.7.2",
"react-refresh": "^0.7.2", "react-refresh": "^0.8.0",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"webpack": "^4.41.5", "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.10.2", "webpack-dev-server": "^3.10.3",
"webpackbar": "^4.0.0" "webpackbar": "^4.0.0"
} }
} }

66
scripts/downloadTop.js Normal file
View File

@ -0,0 +1,66 @@
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()

View File

@ -1,7 +1,6 @@
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'
@ -9,6 +8,11 @@ import { LangSelect } from './components/LangSelect'
import { Container } from './styles/Container' import { Container } from './styles/Container'
import { Flex, FlexMain } from './styles/Flex' 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'
export const App = () => { export const App = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -28,9 +32,21 @@ export const App = () => {
<main> <main>
<Container> <Container>
<h2>Widget</h2> <h2>Widget</h2>
<RsvpReader></RsvpReader> <RsvpWidget />
<h2>Text input</h2> <Flex>
<h2>Options</h2> <FlexMain padding="10px">
<h2>Options</h2>
<Options />
</FlexMain>
<FlexMain>
<h2>Text input</h2>
<TextInput />
</FlexMain>
</Flex>
<h2>Text Output</h2>
<TextOutput />
<h2>Time needed</h2>
<TotalTime />
</Container> </Container>
</main> </main>
<footer> <footer>

View File

@ -1,5 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useSelector } from 'store'
import { selectOffset } from 'store/selectors'
/** /**
* Wrapper that is of double width while hiding its overflow. * Wrapper that is of double width while hiding its overflow.
@ -21,3 +23,8 @@ export const Offset = ({ offset = 0, children }) => {
Offset.propTypes = { Offset.propTypes = {
offset: PropTypes.number offset: PropTypes.number
} }
export const RsvpOffset = ({ children }) => {
const offset = useSelector(selectOffset)
return <Offset offset={offset}>{children}</Offset>
}

View File

@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import { debounce } from 'debounce'
import { Slider } from './Slider' import { Slider } from './Slider'
import { selectOffset, selectLang } from '../store/selectors' import { selectOffset, selectLang } from '../store/selectors'
@ -17,20 +16,19 @@ export const Options = () => {
const { setMaxLength, setWpm, setOffset, setLang } = useDispatch() 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={debounce(val => setMaxLength(val), 100)} onChange={val => setMaxLength(val)}
/> />
<Slider <Slider
title={t('options.wpm')} title={t('options.wpm')}
min={100} min={100}
max={1000} max={1000}
value={wpm} value={wpm}
onChange={debounce(val => setWpm(val), 50)} onChange={val => setWpm(val)}
/> />
<Slider <Slider
title={t('options.offset')} title={t('options.offset')}

View File

@ -1,39 +1,20 @@
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 { Progress } from './Progress'
import { TotalTime } from './TotalTime' import { TotalTime } from './TotalTime'
import { Indicator } from './Indicator' import { RsvpWidget } from './RsvpWidget'
import { Offset } from './Offset'
import { useSelector } from '../store'
import { selectCurrentSegment, selectOffset } from '../store/selectors'
export const RsvpReader = () => { export const RsvpReader = () => {
const segment = useSelector(selectCurrentSegment)
const offset = useSelector(selectOffset)
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.item}> <div className={styles.item}>
<TextInput /> <TextInput />
</div> </div>
<div className={styles.mainItem}> <div className={styles.mainItem}>
<Progress /> <RsvpWidget />
<Offset offset={offset}>
<Indicator>
<Segment>{segment}</Segment>
</Indicator>
</Offset>
<div className={styles.controls}>
<SegmentControl>
<PlayerControl />
</SegmentControl>
</div>
<Options></Options> <Options></Options>
<TotalTime /> <TotalTime />
</div> </div>

View File

@ -0,0 +1,25 @@
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>
</>
)
}

View File

@ -3,6 +3,8 @@ import PropTypes from 'prop-types'
import styles from './Segment.css' import styles from './Segment.css'
import { pivotize } from '../lib/pivotize' import { pivotize } from '../lib/pivotize'
import { selectCurrentSegment } from 'store/selectors'
import { useSelector } from 'store'
export const Segment = ({ children = '' }) => { export const Segment = ({ children = '' }) => {
const [prefix, pivot, suffix] = pivotize(children) const [prefix, pivot, suffix] = pivotize(children)
@ -18,3 +20,8 @@ export const Segment = ({ children = '' }) => {
Segment.propTypes = { Segment.propTypes = {
children: PropTypes.string.isRequired children: PropTypes.string.isRequired
} }
export const RsvpSegment = () => {
const segment = useSelector(selectCurrentSegment)
return <Segment>{segment}</Segment>
}

View File

@ -1,7 +1,8 @@
.area { .area {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
height: 300px; height: 100px;
resize: vertical;
} }
.load { .load {

View File

@ -12,10 +12,5 @@ function formatTime(totalSeconds) {
export const TotalTime = () => { export const TotalTime = () => {
const millis = useSelector(selectTotalTime) const millis = useSelector(selectTotalTime)
return ( return <span>Time needed for Text: {formatTime(millis / 1000)}</span>
<div>
<h2>Time needed</h2>
Time needed for Text: {formatTime(millis / 1000)}
</div>
)
} }

View File

@ -36,7 +36,7 @@ const thunks = {
decWord: () => {}, decWord: () => {},
incSentence: () => {}, incSentence: () => {},
decSentence: () => {}, decSentence: () => {},
setMaxLength: length => ({ length }), setMaxLength: maxLength => ({ maxLength }),
setWpm: wpm => ({ wpm }), setWpm: wpm => ({ wpm }),
setOffset: offset => ({ offset }), setOffset: offset => ({ offset }),
start: () => {}, start: () => {},
@ -72,7 +72,7 @@ const store = createStore({
reducer, reducer,
thunks, thunks,
initialState, initialState,
logging: true, logging: false,
warnOnUndefinedSelect: true warnOnUndefinedSelect: true
}) })
export const Provider = store.Provider export const Provider = store.Provider

View File

@ -10,6 +10,10 @@ export const Flex = ({ children, ...rest }) => {
) )
} }
export const FlexMain = ({ children }) => { export const FlexMain = ({ children, ...rest }) => {
return <div className={styles.mainItem}>{children}</div> return (
<div className={styles.mainItem} style={rest}>
{children}
</div>
)
} }

View File

@ -1,5 +1,6 @@
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')
@ -58,8 +59,9 @@ module.exports = (env, argv) => {
name: entrypoint => `runtime-${entrypoint.name}` name: entrypoint => `runtime-${entrypoint.name}`
} }
}, },
resolve: { modules: ['src', 'lib', 'node_modules'] }, 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