Compare commits
18 Commits
294b66b935
...
0db5d43409
Author | SHA1 | Date | |
---|---|---|---|
0db5d43409 | |||
c3f5fdcd18 | |||
46f3d1f095 | |||
61d85838ae | |||
7649882995 | |||
3de88441fd | |||
4e64f45f06 | |||
9659655e27 | |||
6ce1e75c21 | |||
bc1edea5bb | |||
118e1cd096 | |||
14ce3b1450 | |||
da2ce4eb08 | |||
bd5c6c0b23 | |||
004b2002f4 | |||
c5ae8033a5 | |||
6bd4240e5d | |||
a14ed6bdc1 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
build/
|
8
babel.config.js
Normal file
8
babel.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module.exports = api => {
|
||||||
|
// caching the babel config
|
||||||
|
api.cache.using(() => process.env.NODE_ENV)
|
||||||
|
return {
|
||||||
|
presets: ['@babel/preset-env', '@babel/preset-react']
|
||||||
|
// plugins: [api.env('development') && 'react-refresh/babel'].filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
3851
package-lock.json
generated
3851
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -6,29 +6,52 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --mode development",
|
"start": "webpack-dev-server --mode development",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
|
"build-fresh": "rm -rf build/ && npm run build",
|
||||||
"test": "tests",
|
"test": "tests",
|
||||||
"lint": "prettier --check src/**/*.js",
|
"lint": "eslint src",
|
||||||
"format": "prettier --write src/**/*.js"
|
"format": "prettier --write src/**/*.js"
|
||||||
},
|
},
|
||||||
"author": "Alfred Melch (dev@melch.pro)",
|
"author": "Alfred Melch (dev@melch.pro)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"classnames": "^2.2.6",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
|
"react-icons": "^3.8.0",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
"redux": "^4.0.4",
|
"redux": "^4.0.4",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0"
|
||||||
"styled-components": "^4.4.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "^7.7.5",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "^7.7.6",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.1.1",
|
||||||
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"css-loader": "^3.3.2",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-config-prettier": "^6.7.0",
|
||||||
|
"eslint-config-react-app": "^5.1.0",
|
||||||
|
"eslint-plugin-flowtype": "^4.5.2",
|
||||||
|
"eslint-plugin-import": "^2.19.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-react": "^7.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^2.3.0",
|
||||||
|
"html-webpack-plugin": "^4.0.0-beta.11",
|
||||||
|
"mini-css-extract-plugin": "^0.8.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
|
"postcss-flexbugs-fixes": "^4.1.0",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"postcss-normalize": "^8.0.1",
|
||||||
|
"postcss-preset-env": "^6.7.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
|
"react-refresh": "^0.7.0",
|
||||||
|
"style-loader": "^1.0.1",
|
||||||
"webpack": "^4.41.2",
|
"webpack": "^4.41.2",
|
||||||
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
"webpack-dev-server": "^3.9.0"
|
"webpack-dev-server": "^3.9.0",
|
||||||
|
"webpackbar": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
postcss.config.js
Normal file
7
postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('postcss-flexbugs-fixes'),
|
||||||
|
require('postcss-preset-env'),
|
||||||
|
require('postcss-normalize')
|
||||||
|
]
|
||||||
|
}
|
10
src/App.js
10
src/App.js
@ -1,13 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { TextInput } from './components/TextInput'
|
|
||||||
import { TextOutput } from './components/TextOutput'
|
|
||||||
import { RsvpReader } from './components/RsvpReader'
|
import { RsvpReader } from './components/RsvpReader'
|
||||||
|
|
||||||
export function App() {
|
import { IconContext } from 'react-icons'
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<IconContext.Provider value={{ color: 'grey', size: '1.4em' }}>
|
||||||
<RsvpReader></RsvpReader>
|
<RsvpReader></RsvpReader>
|
||||||
</div>
|
</IconContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useDispatch } from 'react-redux'
|
|
||||||
import {
|
|
||||||
incrementSentence,
|
|
||||||
incrementSegment,
|
|
||||||
incrementWord
|
|
||||||
} from '../store/actions'
|
|
||||||
|
|
||||||
export const MainControl = () => {
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => dispatch(incrementSegment())}>></button>
|
|
||||||
<button onClick={() => dispatch(incrementWord())}>Word></button>
|
|
||||||
<button onClick={() => dispatch(incrementSentence())}>Sentence></button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
31
src/components/Options.js
Normal file
31
src/components/Options.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
|
import { setMaxLength, setWpm } from '../store/actions'
|
||||||
|
|
||||||
|
export const Options = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const maxLength = useSelector(state => state.maxLength)
|
||||||
|
const wpm = useSelector(state => state.wpm)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="3"
|
||||||
|
max="14"
|
||||||
|
value={maxLength}
|
||||||
|
onChange={e => dispatch(setMaxLength(e.target.value))}
|
||||||
|
/>
|
||||||
|
{maxLength}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="100"
|
||||||
|
max="1000"
|
||||||
|
value={wpm}
|
||||||
|
onChange={e => dispatch(setWpm(e.target.value))}
|
||||||
|
/>
|
||||||
|
{wpm}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
38
src/components/PlayerControl.js
Normal file
38
src/components/PlayerControl.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import {
|
||||||
|
hasNextSegment,
|
||||||
|
selectRunning,
|
||||||
|
selectInterval
|
||||||
|
} from '../store/selectors'
|
||||||
|
import { incrementSegment, stop, pause, start } from '../store/actions'
|
||||||
|
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
|
||||||
|
import { useInterval } from './generics/useInterval'
|
||||||
|
import { IconButton } from '../styles/IconButton'
|
||||||
|
|
||||||
|
export const PlayerControl = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const running = useSelector(selectRunning)
|
||||||
|
const hasNext = useSelector(hasNextSegment)
|
||||||
|
const interval = useSelector(selectInterval)
|
||||||
|
|
||||||
|
useInterval(
|
||||||
|
() => {
|
||||||
|
if (!hasNext) dispatch(pause())
|
||||||
|
else dispatch(incrementSegment())
|
||||||
|
},
|
||||||
|
running ? interval : null
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton Icon={FiSquare} onClick={() => dispatch(stop())} />
|
||||||
|
<IconButton
|
||||||
|
Icon={running ? FiPause : FiPlay}
|
||||||
|
onClick={() => dispatch(running ? pause() : start())}
|
||||||
|
disabled={!hasNext}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
13
src/components/RsvpReader.css
Normal file
13
src/components/RsvpReader.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainItem {
|
||||||
|
composes: item;
|
||||||
|
flex: 2;
|
||||||
|
}
|
@ -1,32 +1,29 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
import { TextInput } from './TextInput'
|
import { TextInput } from './TextInput'
|
||||||
import { TextOutput } from './TextOutput'
|
import { TextOutput } from './TextOutput'
|
||||||
import { MainControl } from './MainControl'
|
import { SegmentControl } from './SegmentControl'
|
||||||
import { RsvpSegment } from './RsvpSegment'
|
import { RsvpSegment } from './RsvpSegment'
|
||||||
|
import { Options } from './Options'
|
||||||
|
import { PlayerControl } from './PlayerControl'
|
||||||
|
|
||||||
const FlexRow = styled.div`
|
import styles from './RsvpReader.css'
|
||||||
display: flex;
|
|
||||||
`
|
|
||||||
|
|
||||||
const FlexItem = styled.div`
|
|
||||||
flex: ${props => props.flex || 1};
|
|
||||||
border: 1px solid black;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const RsvpReader = () => {
|
export const RsvpReader = () => {
|
||||||
return (
|
return (
|
||||||
<FlexRow>
|
<div className={styles.container}>
|
||||||
<FlexItem>
|
<div className={styles.item}>
|
||||||
<TextInput />
|
<TextInput />
|
||||||
</FlexItem>
|
</div>
|
||||||
<FlexItem flex={2}>
|
<div className={styles.mainItem}>
|
||||||
<RsvpSegment />
|
<RsvpSegment />
|
||||||
<MainControl></MainControl>
|
<SegmentControl>
|
||||||
</FlexItem>
|
<PlayerControl />
|
||||||
<FlexItem>
|
</SegmentControl>
|
||||||
|
<Options></Options>
|
||||||
|
</div>
|
||||||
|
<div className={styles.item}>
|
||||||
<TextOutput />
|
<TextOutput />
|
||||||
</FlexItem>
|
</div>
|
||||||
</FlexRow>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
46
src/components/SegmentControl.js
Normal file
46
src/components/SegmentControl.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import {
|
||||||
|
FiSkipBack,
|
||||||
|
FiSkipForward,
|
||||||
|
FiRewind,
|
||||||
|
FiFastForward
|
||||||
|
} from 'react-icons/fi'
|
||||||
|
|
||||||
|
import { IconButton } from '../styles/IconButton'
|
||||||
|
|
||||||
|
import {
|
||||||
|
incrementSentence,
|
||||||
|
incrementWord,
|
||||||
|
decrementSentence,
|
||||||
|
decrementWord
|
||||||
|
} from '../store/actions'
|
||||||
|
|
||||||
|
export const SegmentControl = ({ children }) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
title="Previous Sentence"
|
||||||
|
onClick={() => dispatch(decrementSentence())}
|
||||||
|
Icon={FiRewind}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
Icon={FiSkipBack}
|
||||||
|
title="Previous Word"
|
||||||
|
onClick={() => dispatch(decrementWord())}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
<IconButton
|
||||||
|
Icon={FiSkipForward}
|
||||||
|
title="Next Word"
|
||||||
|
onClick={() => dispatch(incrementWord())}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
Icon={FiFastForward}
|
||||||
|
title="Next Sentence"
|
||||||
|
onClick={() => dispatch(incrementSentence())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,20 +1,8 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { setText } from '../store/actions.js'
|
import { setText } from '../store/actions.js'
|
||||||
|
|
||||||
const Button = styled.button`
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: white;
|
|
||||||
color: grey;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: lightgrey;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const lorem =
|
const lorem =
|
||||||
'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.'
|
'Excepteur aliqua cupidatat ullamco laboris cupidatat elit sint cillum incididunt. Anim sit excepteur laboris commodo ullamco consequat tempor. Velit elit eiusmod aute aliquip amet sunt minim deserunt voluptate esse ea sint. Commodo ipsum dolor dolor Lorem et consectetur minim ut in voluptate. Nulla qui consectetur nostrud sint anim minim duis qui amet. Ipsum reprehenderit eiusmod quis Lorem. Consectetur ipsum quis incididunt proident ea sit mollit veniam in excepteur.'
|
||||||
|
|
||||||
@ -27,7 +15,7 @@ export const TextInput = () => {
|
|||||||
defaultValue={text}
|
defaultValue={text}
|
||||||
onInput={e => setTextState(e.target.value)}
|
onInput={e => setTextState(e.target.value)}
|
||||||
></textarea>
|
></textarea>
|
||||||
<Button onClick={() => dispatch(setText(text))}>Load</Button>
|
<button onClick={() => dispatch(setText(text))}>Load</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
7
src/components/TextOutput.css
Normal file
7
src/components/TextOutput.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.current {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sentenceBeginning {
|
||||||
|
color: red;
|
||||||
|
}
|
@ -1,36 +1,39 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
import { selectParsedText } from '../store/selectors.js'
|
import { getNextSmallerNumber } from '../lib/array-util.js'
|
||||||
|
import {
|
||||||
|
selectParsedText,
|
||||||
|
selectCurrentSegmentIndex
|
||||||
|
} from '../store/selectors.js'
|
||||||
|
|
||||||
const Segment = styled.span`
|
import styles from './TextOutput.css'
|
||||||
color: ${props => (props.sentenceBeginning ? 'red' : 'black')};
|
|
||||||
font-weight: ${props => (props.current ? 'bold' : 'normal')};
|
|
||||||
`
|
|
||||||
|
|
||||||
const SelectedSegment = styled.span`
|
|
||||||
font-weight: ;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const TextOutput = () => {
|
export const TextOutput = () => {
|
||||||
const parsedText = useSelector(selectParsedText)
|
const { segments, sentences, words } = useSelector(selectParsedText)
|
||||||
const curSegment = useSelector(state => state.curSegment)
|
const curSegment = useSelector(selectCurrentSegmentIndex)
|
||||||
const { segments, words, sentences } = parsedText
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{segments.map((segment, idx) => {
|
{segments.map((segment, idx) => {
|
||||||
const current = curSegment === idx
|
const isCurrent = curSegment === idx
|
||||||
const sentenceBeginning = sentences.includes(idx)
|
const isWordStart = words.includes(idx)
|
||||||
|
const wordBeginning = isWordStart
|
||||||
|
? idx
|
||||||
|
: getNextSmallerNumber(idx, words)
|
||||||
|
const isSentenceStart = sentences.includes(wordBeginning)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Segment
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
current={current}
|
className={classNames({
|
||||||
sentenceBeginning={sentenceBeginning}
|
[styles.current]: isCurrent,
|
||||||
|
[styles.sentenceBeginning]: isSentenceStart
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{segment}{' '}
|
{isWordStart && ' '}
|
||||||
</Segment>
|
{segment}
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
75
src/components/generics/Player.js
Normal file
75
src/components/generics/Player.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { FiPlay, FiPause, FiSquare } from 'react-icons/fi'
|
||||||
|
|
||||||
|
function safeCall(fn) {
|
||||||
|
typeof fn === 'function' && fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Player = ({ onTick, onStart, onStop, onPause, delay }) => {
|
||||||
|
const [isRunning, start, stop] = usePlayer(stop => onTick(stop), delay)
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
safeCall(onStart)
|
||||||
|
start()
|
||||||
|
console.log('yay')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
safeCall(onStop)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
safeCall(onPause)
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={handleStop}>
|
||||||
|
<FiSquare />
|
||||||
|
</button>
|
||||||
|
{isRunning && (
|
||||||
|
<button onClick={handlePause}>
|
||||||
|
<FiPause />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!isRunning && (
|
||||||
|
<button onClick={handleStart}>
|
||||||
|
<FiPlay />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePlayer(onTick, delay) {
|
||||||
|
const [isRunning, setRunning] = useState(false)
|
||||||
|
const start = () => setRunning(true)
|
||||||
|
const stop = () => setRunning(false)
|
||||||
|
|
||||||
|
useInterval(() => onTick(stop), isRunning ? delay : null)
|
||||||
|
return [isRunning, start, stop]
|
||||||
|
}
|
||||||
|
|
||||||
|
// from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||||
|
function useInterval(callback, delay) {
|
||||||
|
const savedCallback = useRef()
|
||||||
|
|
||||||
|
// Remember the latest callback.
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback
|
||||||
|
}, [callback])
|
||||||
|
|
||||||
|
// Set up the interval.
|
||||||
|
useEffect(() => {
|
||||||
|
function tick() {
|
||||||
|
savedCallback.current()
|
||||||
|
}
|
||||||
|
if (delay !== null) {
|
||||||
|
let id = setInterval(tick, delay)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
}, [delay])
|
||||||
|
}
|
21
src/components/generics/useInterval.js
Normal file
21
src/components/generics/useInterval.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useRef, useEffect } from 'react'
|
||||||
|
|
||||||
|
// from: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||||
|
export function useInterval(callback, delay) {
|
||||||
|
const savedCallback = useRef()
|
||||||
|
// Remember the latest callback.
|
||||||
|
useEffect(() => {
|
||||||
|
savedCallback.current = callback
|
||||||
|
}, [callback])
|
||||||
|
|
||||||
|
// Set up the interval.
|
||||||
|
useEffect(() => {
|
||||||
|
function tick() {
|
||||||
|
savedCallback.current()
|
||||||
|
}
|
||||||
|
if (delay !== null) {
|
||||||
|
let id = setInterval(tick, delay)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}
|
||||||
|
}, [delay])
|
||||||
|
}
|
@ -12,10 +12,9 @@ function createRootElement() {
|
|||||||
body.appendChild(root)
|
body.appendChild(root)
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App></App>
|
<App />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
createRootElement()
|
createRootElement()
|
||||||
)
|
)
|
||||||
|
@ -25,6 +25,6 @@ export function getNextSmallerNumber(num, sortedArray) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastEntry(arr) {
|
export function lastEntry(arr) {
|
||||||
return arr[arr.length - 1]
|
return arr[arr.length - 1]
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
export const setText = text => ({ type: 'SET_TEXT', payload: text })
|
export const setText = text => ({ type: 'SET_TEXT', payload: text })
|
||||||
export const incrementSegment = () => ({ type: 'INCREMENT_SEGMENT' })
|
export const resetSegment = () => ({ type: 'SET_CURRENT_SEGMENT', payload: 0 })
|
||||||
export const incrementWord = () => ({ type: 'INCREMENT_WORD' })
|
export const incrementSegment = () => ({ type: 'INC_SEGMENT' })
|
||||||
export const incrementSentence = () => ({ type: 'INCREMENT_SENTENCE' })
|
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 start = () => ({ type: 'START' })
|
||||||
|
export const stop = () => ({ type: 'STOP' })
|
||||||
|
export const pause = () => ({ type: 'PAUSE' })
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
import { selectNextWord, selectNextSentence } from './selectors'
|
import {
|
||||||
|
selectNextWord,
|
||||||
|
selectNextSentence,
|
||||||
|
selectPrevWord,
|
||||||
|
selectPrevSentence
|
||||||
|
} from './selectors'
|
||||||
|
|
||||||
export const initialState = {
|
export const initialState = {
|
||||||
originalText: 'Sample Text',
|
originalText: 'Sample Text',
|
||||||
curSegment: 0
|
curIdx: 0,
|
||||||
|
maxLength: 5,
|
||||||
|
isPlaying: false,
|
||||||
|
wpm: 300,
|
||||||
|
running: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const reducer = {
|
const reducer = {
|
||||||
SET_TEXT: (state, payload) => ({
|
SET_TEXT: (state, payload) => ({
|
||||||
...state,
|
...state,
|
||||||
originalText: payload,
|
originalText: payload,
|
||||||
curSegment: 0
|
curIdx: 0
|
||||||
}),
|
}),
|
||||||
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curSegment: payload }),
|
SET_CURRENT_SEGMENT: (state, payload) => ({ ...state, curIdx: payload }),
|
||||||
INCREMENT_SEGMENT: state => ({
|
INC_SEGMENT: state => ({ ...state, curIdx: state.curIdx + 1 }),
|
||||||
...state,
|
DEC_SEGMENT: state => ({ ...state, curIdx: state.curIdx - 1 }),
|
||||||
curSegment: state.curSegment + 1
|
INC_WORD: state => ({ ...state, curIdx: selectNextWord(state) }),
|
||||||
}),
|
DEC_WORD: state => ({ ...state, curIdx: selectPrevWord(state) }),
|
||||||
INCREMENT_WORD: state => ({ ...state, curSegment: selectNextWord(state) }),
|
INC_SENTENCE: state => ({ ...state, curIdx: selectNextSentence(state) }),
|
||||||
INCREMENT_SENTENCE: state => ({
|
DEC_SENTENCE: state => ({ ...state, curIdx: selectPrevSentence(state) }),
|
||||||
...state,
|
SET_MAX_LENGTH: (state, payload) => ({ ...state, maxLength: payload }),
|
||||||
curSegment: selectNextSentence(state)
|
SET_WPM: (state, payload) => ({ ...state, wpm: payload }),
|
||||||
})
|
START: state => ({ ...state, running: true }),
|
||||||
|
STOP: state => ({ ...state, running: false, curIdx: 0 }),
|
||||||
|
PAUSE: state => ({ ...state, running: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducerFn = (state, { type, payload }) => {
|
export const reducerFn = (state, { type, payload }) => {
|
||||||
|
@ -5,9 +5,12 @@ import { getNextBiggerNumber, getNextSmallerNumber } from '../lib/array-util.js'
|
|||||||
|
|
||||||
// parsedText selectors
|
// parsedText selectors
|
||||||
|
|
||||||
|
export const selectMaxLength = state => state.maxLength
|
||||||
|
|
||||||
export const selectParsedText = createSelector(
|
export const selectParsedText = createSelector(
|
||||||
state => state.originalText,
|
state => state.originalText,
|
||||||
originalText => parseText(originalText)
|
selectMaxLength,
|
||||||
|
(originalText, maxLength) => parseText(originalText, maxLength)
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectSegments = createSelector(
|
export const selectSegments = createSelector(
|
||||||
@ -15,15 +18,20 @@ export const selectSegments = createSelector(
|
|||||||
parsedText => parsedText.segments
|
parsedText => parsedText.segments
|
||||||
)
|
)
|
||||||
|
|
||||||
export const selectCurrentSegmentIndex = createSelector(
|
export const selectCurrentSegmentIndex = state => state.curIdx
|
||||||
state => state.curSegment,
|
|
||||||
curIdx => curIdx
|
|
||||||
)
|
|
||||||
|
|
||||||
export const selectCurrentSegment = createSelector(
|
export const selectCurrentSegment = createSelector(
|
||||||
selectParsedText,
|
selectParsedText,
|
||||||
state => state.curSegment,
|
selectCurrentSegmentIndex,
|
||||||
(parsedText, curSegment) => parsedText.segments[curSegment]
|
(parsedText, curIdx) => parsedText.segments[curIdx]
|
||||||
|
)
|
||||||
|
|
||||||
|
export const hasNextSegment = createSelector(
|
||||||
|
selectSegments,
|
||||||
|
selectCurrentSegmentIndex,
|
||||||
|
(segments, idx) => {
|
||||||
|
return idx < segments.length - 1
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// next/prev words
|
// next/prev words
|
||||||
@ -35,7 +43,7 @@ export const selectWords = createSelector(
|
|||||||
|
|
||||||
export const selectNextWord = createSelector(
|
export const selectNextWord = createSelector(
|
||||||
selectWords,
|
selectWords,
|
||||||
state => state.curSegment,
|
selectCurrentSegmentIndex,
|
||||||
(words, curSegment) => getNextBiggerNumber(curSegment, words)
|
(words, curSegment) => getNextBiggerNumber(curSegment, words)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,7 +54,6 @@ export const selectPrevWord = createSelector(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const selectHasNextWord = createSelector(selectNextWord, Boolean)
|
export const selectHasNextWord = createSelector(selectNextWord, Boolean)
|
||||||
|
|
||||||
export const selectHasPrevWord = createSelector(selectPrevWord, Boolean)
|
export const selectHasPrevWord = createSelector(selectPrevWord, Boolean)
|
||||||
|
|
||||||
// next/prev sentences
|
// next/prev sentences
|
||||||
@ -70,3 +77,7 @@ export const selectPrevSentence = createSelector(
|
|||||||
|
|
||||||
export const selectHasNextSentence = createSelector(selectNextSentence, Boolean)
|
export const selectHasNextSentence = createSelector(selectNextSentence, Boolean)
|
||||||
export const selectHasPrevSentence = createSelector(selectPrevSentence, Boolean)
|
export const selectHasPrevSentence = createSelector(selectPrevSentence, Boolean)
|
||||||
|
|
||||||
|
export const selectRunning = state => state.running
|
||||||
|
export const selectWpm = state => state.wpm
|
||||||
|
export const selectInterval = createSelector(selectWpm, wpm => 60000 / wpm)
|
||||||
|
42
src/styles/IconButton.css
Normal file
42
src/styles/IconButton.css
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.svgButton {
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
/* width: 1.4em; */
|
||||||
|
/* height: 1.4em; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgButton svg {
|
||||||
|
outline: none;
|
||||||
|
transition: transform 0.1s linear;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgButton:focus {
|
||||||
|
outline: 2px dashed #17171d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgButton:hover svg {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgButton::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgButton[disabled] svg {
|
||||||
|
transform: scale(1.5);
|
||||||
|
|
||||||
|
color: '#c7c7c7';
|
||||||
|
}
|
||||||
|
|
||||||
|
.a11yLabel {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
22
src/styles/IconButton.js
Normal file
22
src/styles/IconButton.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import styles from './IconButton.css'
|
||||||
|
|
||||||
|
const SvgButton = ({ children, ...props }) => (
|
||||||
|
<button className={styles.svgButton} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const AccessLabel = ({ children }) => (
|
||||||
|
<span className={styles.a11yLabel}>{children}</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const IconButton = ({ Icon, title, ...props }) => {
|
||||||
|
return (
|
||||||
|
<SvgButton tabIndex="0" title={title} {...props}>
|
||||||
|
{Icon && <Icon aria-hidden="true" focusable="false" />}
|
||||||
|
<AccessLabel>{title}</AccessLabel>
|
||||||
|
</SvgButton>
|
||||||
|
)
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
import { Chapter } from './Chapter.js'
|
|
||||||
import { Player } from './Player.js'
|
|
||||||
|
|
||||||
export class Book {
|
|
||||||
constructor() {
|
|
||||||
this.chapters = [new Chapter('')]
|
|
||||||
this.curIdx = 0
|
|
||||||
this.player = new Player()
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentChapter() {
|
|
||||||
return this.chapters(curIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
addChapter(chapter) {
|
|
||||||
this.chapters.push(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChapter(chapter) {
|
|
||||||
this.chapters.filter(el => el !== chapter)
|
|
||||||
if (this.curIdx > 0) {
|
|
||||||
this.curIdx--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
import { parseText } from './textProcessing/parseText.js'
|
|
||||||
|
|
||||||
export class Chapter {
|
|
||||||
constructor(text, maxLength = -1) {
|
|
||||||
this._originalText = text
|
|
||||||
this._maxLength = maxLength
|
|
||||||
this.initSegments()
|
|
||||||
}
|
|
||||||
|
|
||||||
initSegments() {
|
|
||||||
let { segments, words, sentences } = parseText(this._text, this._maxLength)
|
|
||||||
this.segments = segments
|
|
||||||
this.words = words
|
|
||||||
this.sentences = sentences
|
|
||||||
this.resetIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
resetIndex() {
|
|
||||||
this.currentIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
setText(text) {
|
|
||||||
this._originalText = text
|
|
||||||
this.initSegments()
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaxLength(maxLength) {
|
|
||||||
this._maxLength = maxLength
|
|
||||||
this.initSegments()
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentSegment() {
|
|
||||||
return this.segments[this.currentIdx]
|
|
||||||
}
|
|
||||||
|
|
||||||
get metainfo() {
|
|
||||||
return {
|
|
||||||
segmentCount: this.segments.length,
|
|
||||||
wordsCount: this.words.length,
|
|
||||||
sentenceCount: this.sentences.length,
|
|
||||||
currentSegment: currentIdx + 1,
|
|
||||||
currentWord: -1,
|
|
||||||
currentSentence: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next() {
|
|
||||||
if (!this.hasNext()) return null
|
|
||||||
this.currentIdx += 1
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
prev() {
|
|
||||||
if (!this.hasPrev()) return null
|
|
||||||
this.currentIdx -= 1
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
nextWord() {
|
|
||||||
if (!this.hasNextWord()) return null
|
|
||||||
this.currentIdx = getNextBiggerNumber(this.currentIdx, this.words)
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
prevWord() {
|
|
||||||
if (!this.hasPrevWord()) return null
|
|
||||||
this.currentIdx = getNextSmallerNumber(this.currentIdx, this.words)
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
nextSentence() {
|
|
||||||
if (!this.hasNextSentence()) return null
|
|
||||||
this.currentIdx = getNextBiggerNumber(this.currentIdx, this.sentences)
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
prevSentence() {
|
|
||||||
if (!this.hasPrevSentence()) return null
|
|
||||||
this.currentIdx = getNextSmallerNumber(this.currentIdx, this.sentences)
|
|
||||||
return this.currentSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNext() {
|
|
||||||
return this.currentIdx < this.segments.length - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPrev() {
|
|
||||||
return this.currentIdx > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNextWord() {
|
|
||||||
return this.currentIdx < lastEntry(this.words)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPrevWord() {
|
|
||||||
return this.currentIdx > this.words[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
hasNextSentence() {
|
|
||||||
return this.currentIdx < lastEntry(this.sentences)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasPrevSentence() {
|
|
||||||
return this.currentIdx > this.sentences[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next bigger number from a sorted Array of numbers.
|
|
||||||
* Returns null if num is the biggest number
|
|
||||||
* @param {Number} idx
|
|
||||||
* @param {Array<Number>} sortedArray
|
|
||||||
*/
|
|
||||||
function getNextBiggerNumber(num, sortedArray) {
|
|
||||||
for (let currentNumber of sortedArray) {
|
|
||||||
if (currentNumber > num) return currentNumber
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next smaller number from a sorted Array of numbers.
|
|
||||||
* Returns null if num is the smallest number
|
|
||||||
* @param {Number} idx
|
|
||||||
* @param {Array<Number>} sortedArray
|
|
||||||
*/
|
|
||||||
function getNextSmallerNumber(num, sortedArray) {
|
|
||||||
let reversedArray = [...sortedArray].reverse()
|
|
||||||
for (let currentNumber of reversedArray) {
|
|
||||||
if (currentNumber < num) return currentNumber
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function lastEntry(arr) {
|
|
||||||
return arr[arr.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const _privates = { getNextBiggerNumber }
|
|
@ -1,45 +0,0 @@
|
|||||||
export class Player {
|
|
||||||
constructor(interval = 200) {
|
|
||||||
this.intervalHandle = null
|
|
||||||
this.interval = interval
|
|
||||||
this.subscribers = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
get playing() {
|
|
||||||
return this.intervalHandle !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
clearInterval(this.intervalHandle)
|
|
||||||
this.intervalHandle = setInterval(this.tick.bind(this), this.interval)
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
clearInterval(this.intervalHandle)
|
|
||||||
this.intervalHandle = null
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
if (this.playing) this.stop()
|
|
||||||
else this.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInterval(interval) {
|
|
||||||
this.interval = interval
|
|
||||||
if (this.intervalHandle) this.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
for (let callback of Object.values(this.subscribers)) {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(name, callback) {
|
|
||||||
this.subscribers[name] = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribe(name) {
|
|
||||||
delete subscribers[name]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import { RSVPPlayer } from './RSVPPlayer.js'
|
|
||||||
|
|
||||||
const initialOptions = {
|
|
||||||
maxLength: 10,
|
|
||||||
autoPlayNextChapter: false,
|
|
||||||
wordsPerMinute: 200,
|
|
||||||
pivotMethod: 'default',
|
|
||||||
pauseOnPunctuation: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RSVPController {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.options = initialOptions
|
|
||||||
this.setOptions(options)
|
|
||||||
|
|
||||||
this.rsvpPlayer = new RSVPPlayer()
|
|
||||||
this.rsvpPlayer.onTick = this.onChange
|
|
||||||
|
|
||||||
this.applyOptions()
|
|
||||||
}
|
|
||||||
|
|
||||||
setOptions(options) {
|
|
||||||
this.options = { ...this.options, ...options }
|
|
||||||
}
|
|
||||||
|
|
||||||
applyOptions() {
|
|
||||||
let { options } = this
|
|
||||||
let { chapter, player } = this.rsvpPlayer
|
|
||||||
chapter.setMaxLength = options.maxLength
|
|
||||||
player.updateInterval(1000 / options.wordsPerMinute)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOptions(options) {
|
|
||||||
this.setOptions(options)
|
|
||||||
this.applyOptions()
|
|
||||||
this.onChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
processAction(action, payload) {
|
|
||||||
let { chapter, player } = this.rsvpPlayer
|
|
||||||
switch (action) {
|
|
||||||
case 'prevSentence':
|
|
||||||
chapter.prevSentence()
|
|
||||||
break
|
|
||||||
case 'nextSentence':
|
|
||||||
chapter.nextSentence()
|
|
||||||
break
|
|
||||||
case 'prevWord':
|
|
||||||
chapter.prevWord()
|
|
||||||
break
|
|
||||||
case 'nextWord':
|
|
||||||
chapter.nextWord()
|
|
||||||
break
|
|
||||||
case 'playpause':
|
|
||||||
player.toggle()
|
|
||||||
break
|
|
||||||
case 'stop':
|
|
||||||
player.stop()
|
|
||||||
chapter.reset()
|
|
||||||
break
|
|
||||||
case 'load':
|
|
||||||
chapter = new Chapter(payload, 10)
|
|
||||||
}
|
|
||||||
this.onChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange() {
|
|
||||||
console.warn('RSVPController: onChange not set')
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Player } from './Player.js'
|
|
||||||
import { Book } from './Book.js'
|
|
||||||
import { Chapter } from './Chapter.js'
|
|
||||||
|
|
||||||
export class RSVPPlayer {
|
|
||||||
constructor(maxLength) {
|
|
||||||
this.chapter = new Chapter(maxLength)
|
|
||||||
this.player = new Player()
|
|
||||||
|
|
||||||
this.player.subscribe('main', this.tick.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
if (!this.chapter.hasNext()) {
|
|
||||||
this.player.stop()
|
|
||||||
} else {
|
|
||||||
this.chapter.next()
|
|
||||||
}
|
|
||||||
this.onTick()
|
|
||||||
}
|
|
||||||
|
|
||||||
onTick() {
|
|
||||||
console.warn('RSVPPlayer: onTick not set')
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,92 @@
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
|
||||||
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
||||||
|
.BundleAnalyzerPlugin
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||||
|
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
|
||||||
|
const WebpackBar = require('webpackbar')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = (env, argv) => {
|
||||||
plugins: [new HtmlWebpackPlugin()],
|
const isEnvProduction = argv.mode === 'production'
|
||||||
devtool: 'source-map',
|
const isEnvDevelopment = argv.mode === 'development'
|
||||||
module: {
|
|
||||||
rules: [
|
return {
|
||||||
{
|
output: {
|
||||||
test: /.js$/,
|
path: path.resolve(__dirname, 'build'),
|
||||||
exclude: /node_modules/,
|
filename: 'static/js/[name].[contenthash:8].js',
|
||||||
loader: 'babel-loader'
|
chunkFilename: 'static/js/[name].[contenthash:8].chunk.js'
|
||||||
|
},
|
||||||
|
devtool: isEnvProduction ? 'source-map' : 'cheap-module-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
// process javascript with babel
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'babel-loader'
|
||||||
|
},
|
||||||
|
// process css. css modules are enabled.
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
isEnvDevelopment && 'style-loader',
|
||||||
|
isEnvProduction && MiniCssExtractPlugin.loader,
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: {
|
||||||
|
localIdentName: isEnvProduction
|
||||||
|
? '[hash:base64]'
|
||||||
|
: '[path][name]__[local]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isEnvProduction,
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
name: false
|
||||||
|
},
|
||||||
|
runtimeChunk: {
|
||||||
|
name: entrypoint => `runtime-${entrypoint.name}`
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
},
|
plugins: [
|
||||||
devServer: {
|
// creat an index.html
|
||||||
quiet: true
|
new HtmlWebpackPlugin(),
|
||||||
|
// show a progress bar
|
||||||
|
new WebpackBar(),
|
||||||
|
// create a report.html for bundle size
|
||||||
|
// extract css to css files
|
||||||
|
isEnvProduction &&
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'static/css/[name].[contenthash:8].css',
|
||||||
|
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
|
||||||
|
}),
|
||||||
|
// use cssnano to minify css
|
||||||
|
isEnvProduction &&
|
||||||
|
new OptimizeCssAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
map: { inline: false, annotation: true }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
isEnvProduction &&
|
||||||
|
new BundleAnalyzerPlugin({
|
||||||
|
analyzerMode: 'static',
|
||||||
|
openAnalyzer: false
|
||||||
|
})
|
||||||
|
// hot reload not working right now
|
||||||
|
// isEnvDevelopment &&
|
||||||
|
// new ReactRefreshWebpackPlugin({ disableRefreshCheck: true })
|
||||||
|
].filter(Boolean),
|
||||||
|
devServer: {
|
||||||
|
stats: 'minimal'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user