Compare commits
6 Commits
067e1ccda6
...
34923713f5
Author | SHA1 | Date | |
---|---|---|---|
34923713f5 | |||
60f92904cc | |||
c2379ff768 | |||
182b20a3ee | |||
8a08342e3c | |||
7ce9aac9cb |
@ -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
3
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
tmp/
|
tmp/
|
||||||
|
books/
|
@ -2,5 +2,5 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "src"
|
"baseUrl": "src"
|
||||||
},
|
},
|
||||||
"include": ["src", "lib"]
|
"include": ["src"]
|
||||||
}
|
}
|
2437
package-lock.json
generated
2437
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@ -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
66
scripts/downloadTop.js
Normal 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()
|
24
src/App.js
24
src/App.js
@ -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>
|
||||||
|
@ -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>
|
||||||
|
}
|
||||||
|
@ -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')}
|
||||||
|
@ -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>
|
||||||
|
25
src/components/RsvpWidget.js
Normal file
25
src/components/RsvpWidget.js
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user