Based on create-react-app --template redux-typescript. Some stuff still needs to be removed or fixed. Tests likely fail. But the basic connection logic works!main
| @ -1,19 +0,0 @@ | |||
| { | |||
| "presets": ["es2015", "react", "stage-1"], | |||
| "env": { | |||
| "development": { | |||
| "plugins": [ | |||
| ["react-transform", { | |||
| "transforms": [{ | |||
| "transform": "react-transform-hmr", | |||
| "imports": ["react"], | |||
| "locals": ["module"] | |||
| }, { | |||
| "transform": "react-transform-catch-errors", | |||
| "imports": ["react", "redbox-react"] | |||
| }] | |||
| }] | |||
| ] | |||
| } | |||
| } | |||
| } | |||
| @ -1,13 +0,0 @@ | |||
| # editorconfig.org | |||
| root = true | |||
| [*] | |||
| indent_style = space | |||
| indent_size = 2 | |||
| end_of_line = lf | |||
| charset = utf-8 | |||
| trim_trailing_whitespace = true | |||
| insert_final_newline = true | |||
| [*.md] | |||
| trim_trailing_whitespace = false | |||
| @ -1,65 +0,0 @@ | |||
| { | |||
| "parser": "babel-eslint", | |||
| "extends": "eslint:recommended", | |||
| "plugins": [ | |||
| "react" | |||
| ], | |||
| "env": { | |||
| "es6": true, | |||
| "browser": true, | |||
| "node": true, | |||
| "jquery": true, | |||
| "mocha": true | |||
| }, | |||
| "ecmaFeatures": { | |||
| "jsx": true, | |||
| "modules": true | |||
| }, | |||
| "rules": { | |||
| "quotes": 0, | |||
| "no-console": 1, | |||
| "no-debugger": 1, | |||
| "no-var": 1, | |||
| "semi": [1, "always"], | |||
| "no-trailing-spaces": 0, | |||
| "eol-last": 0, | |||
| "no-unused-vars": 0, | |||
| "no-underscore-dangle": 0, | |||
| "no-alert": 0, | |||
| "no-lone-blocks": 0, | |||
| "jsx-quotes": 1, | |||
| "react/display-name": [ 1, {"ignoreTranspilerName": false }], | |||
| "react/forbid-prop-types": [1, {"forbid": ["any"]}], | |||
| "react/jsx-boolean-value": 1, | |||
| "react/jsx-closing-bracket-location": 0, | |||
| "react/jsx-curly-spacing": 1, | |||
| "react/jsx-indent-props": 0, | |||
| "react/jsx-key": 1, | |||
| "react/jsx-max-props-per-line": 0, | |||
| "react/jsx-no-bind": 0, | |||
| "react/jsx-no-duplicate-props": 1, | |||
| "react/jsx-no-literals": 0, | |||
| "react/jsx-no-undef": 1, | |||
| "react/jsx-pascal-case": 1, | |||
| "react/jsx-sort-prop-types": 0, | |||
| "react/jsx-sort-props": 0, | |||
| "react/jsx-uses-react": 1, | |||
| "react/jsx-uses-vars": 1, | |||
| "react/no-danger": 1, | |||
| "react/no-did-mount-set-state": 1, | |||
| "react/no-did-update-set-state": 1, | |||
| "react/no-direct-mutation-state": 1, | |||
| "react/no-multi-comp": 1, | |||
| "react/no-set-state": 1, | |||
| "react/no-unknown-property": 1, | |||
| "react/prefer-es6-class": 1, | |||
| "react/prop-types": 1, | |||
| "react/react-in-jsx-scope": 1, | |||
| "react/require-extension": 1, | |||
| "react/self-closing-comp": 1, | |||
| "react/sort-comp": 1, | |||
| "react/wrap-multilines": 1 | |||
| }, | |||
| "globals": { | |||
| } | |||
| } | |||
| @ -1,39 +1,23 @@ | |||
| # Logs | |||
| logs | |||
| *.log | |||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||
| # Runtime data | |||
| pids | |||
| *.pid | |||
| *.seed | |||
| # dependencies | |||
| /node_modules | |||
| /.pnp | |||
| .pnp.js | |||
| # Directory for instrumented libs generated by jscoverage/JSCover | |||
| lib-cov | |||
| # testing | |||
| /coverage | |||
| # Coverage directory used by tools like istanbul | |||
| coverage | |||
| # production | |||
| /build | |||
| # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||
| .grunt | |||
| # node-waf configuration | |||
| .lock-wscript | |||
| # Compiled binary addons (http://nodejs.org/api/addons.html) | |||
| build/Release | |||
| # Dependency directory | |||
| # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git | |||
| node_modules | |||
| #dist folder | |||
| dist | |||
| #Webstorm metadata | |||
| .idea | |||
| # Mac files | |||
| # misc | |||
| .DS_Store | |||
| # Vim session file | |||
| Session.vim | |||
| .env.local | |||
| .env.development.local | |||
| .env.test.local | |||
| .env.production.local | |||
| npm-debug.log* | |||
| yarn-debug.log* | |||
| yarn-error.log* | |||
| @ -1 +0,0 @@ | |||
| save-exact=true | |||
| @ -0,0 +1,44 @@ | |||
| This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template. | |||
| ## Available Scripts | |||
| In the project directory, you can run: | |||
| ### `npm start` | |||
| Runs the app in the development mode.<br /> | |||
| Open [http://localhost:3000](http://localhost:3000) to view it in the browser. | |||
| The page will reload if you make edits.<br /> | |||
| You will also see any lint errors in the console. | |||
| ### `npm test` | |||
| Launches the test runner in the interactive watch mode.<br /> | |||
| See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. | |||
| ### `npm run build` | |||
| Builds the app for production to the `build` folder.<br /> | |||
| It correctly bundles React in production mode and optimizes the build for the best performance. | |||
| The build is minified and the filenames include the hashes.<br /> | |||
| Your app is ready to be deployed! | |||
| See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. | |||
| ### `npm run eject` | |||
| **Note: this is a one-way operation. Once you `eject`, you can’t go back!** | |||
| If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. | |||
| Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. | |||
| You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. | |||
| ## Learn More | |||
| You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). | |||
| To learn React, check out the [React documentation](https://reactjs.org/). | |||
| @ -1,90 +1,53 @@ | |||
| { | |||
| "name": "react-slingshot", | |||
| "version": "1.2.0", | |||
| "description": "Starter kit for creating apps with React and Redux", | |||
| "name": "solstice-web", | |||
| "version": "0.1.0", | |||
| "private": true, | |||
| "dependencies": { | |||
| "@reduxjs/toolkit": "^1.6.1", | |||
| "@testing-library/jest-dom": "^4.2.4", | |||
| "@testing-library/react": "^9.5.0", | |||
| "@testing-library/user-event": "^7.2.1", | |||
| "final-form": "^4.20.2", | |||
| "immutable": "^3", | |||
| "md5": "^2.3.0", | |||
| "react": "^17.0.2", | |||
| "react-dom": "^17.0.2", | |||
| "react-final-form": "^6.5.3", | |||
| "react-redux": "^7.2.4", | |||
| "react-router-dom": "^5.2.0", | |||
| "react-scripts": "4.0.3", | |||
| "typescript": "~4.1.5" | |||
| }, | |||
| "scripts": { | |||
| "prestart": "npm run remove-dist", | |||
| "start": "parallelshell \"npm run lint:tools\" \"npm run test:watch\" \"npm run open:src\"", | |||
| "open:src": "babel-node tools/srcServer.js", | |||
| "open:dist": "babel-node tools/distServer.js", | |||
| "lint:tools": "eslint webpack.config.js tools", | |||
| "clean-dist": "npm run remove-dist && mkdir dist", | |||
| "remove-dist": "node_modules/.bin/rimraf ./dist", | |||
| "build:html": "babel-node tools/buildHtml.js", | |||
| "prebuild": "npm run clean-dist && npm run build:html", | |||
| "build": "npm run test && babel-node tools/build.js && npm run open:dist", | |||
| "test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --recursive \"./src/**/*.spec.js\"", | |||
| "test:watch": "npm run test -- --watch" | |||
| "start": "react-scripts start", | |||
| "build": "react-scripts build", | |||
| "test": "react-scripts test", | |||
| "eject": "react-scripts eject" | |||
| }, | |||
| "author": "Cory House", | |||
| "license": "MIT", | |||
| "dependencies": { | |||
| "immutable": "~3.7.6", | |||
| "md5": "2.1.0", | |||
| "object-assign": "4.0.1", | |||
| "react": "0.14.7", | |||
| "react-dom": "0.14.7", | |||
| "react-immutable-proptypes": "~1.7.0", | |||
| "react-redux": "4.4.0", | |||
| "react-router": "2.4.1", | |||
| "redux": "3.3.1", | |||
| "redux-form": "~4.2.0", | |||
| "redux-logger": "~2.6.1", | |||
| "redux-promise": "~0.5.1", | |||
| "redux-thunk": "~2.0.1" | |||
| "eslintConfig": { | |||
| "extends": "react-app" | |||
| }, | |||
| "devDependencies": { | |||
| "babel-cli": "6.5.1", | |||
| "babel-core": "6.5.1", | |||
| "babel-eslint": "5.0.0", | |||
| "babel-loader": "6.2.3", | |||
| "babel-plugin-react-display-name": "2.0.0", | |||
| "babel-plugin-react-transform": "2.0.0", | |||
| "babel-preset-es2015": "6.5.0", | |||
| "babel-preset-react": "6.5.0", | |||
| "babel-preset-stage-1": "6.5.0", | |||
| "browser-sync": "2.11.1", | |||
| "chai": "3.4.1", | |||
| "cheerio": "0.19.0", | |||
| "colors": "1.1.2", | |||
| "cross-env": "1.0.7", | |||
| "css-loader": "0.23.1", | |||
| "eslint": "2.2.0", | |||
| "eslint-loader": "1.3.0", | |||
| "eslint-plugin-react": "4.0.0", | |||
| "extract-text-webpack-plugin": "1.0.1", | |||
| "file-loader": "0.8.5", | |||
| "mocha": "2.3.4", | |||
| "node-sass": "3.4.2", | |||
| "parallelshell": "2.0.0", | |||
| "react-transform-catch-errors": "1.0.1", | |||
| "react-transform-hmr": "1.0.1", | |||
| "redbox-react": "1.2.0", | |||
| "rimraf": "2.5.0", | |||
| "sass-loader": "3.1.2", | |||
| "style-loader": "0.13.0", | |||
| "watch": "0.17.1", | |||
| "webpack": "1.12.11", | |||
| "webpack-dev-middleware": "1.4.0", | |||
| "webpack-hot-middleware": "2.6.0", | |||
| "yargs": "3.32.0" | |||
| "browserslist": { | |||
| "production": [ | |||
| ">0.2%", | |||
| "not dead", | |||
| "not op_mini all" | |||
| ], | |||
| "development": [ | |||
| "last 1 chrome version", | |||
| "last 1 firefox version", | |||
| "last 1 safari version" | |||
| ] | |||
| }, | |||
| "keywords:": [ | |||
| "react", | |||
| "reactjs", | |||
| "hot", | |||
| "reload", | |||
| "hmr", | |||
| "live", | |||
| "edit", | |||
| "webpack", | |||
| "redux", | |||
| "flux", | |||
| "boilerplate", | |||
| "starter" | |||
| ], | |||
| "repository": { | |||
| "type": "git", | |||
| "url": "https://github.com/coryhouse/react-slingshot" | |||
| "devDependencies": { | |||
| "@types/jest": "^24.9.1", | |||
| "@types/node": "^12.20.17", | |||
| "@types/react": "^16.14.11", | |||
| "@types/react-dom": "^16.9.14", | |||
| "@types/react-redux": "^7.1.18", | |||
| "@types/react-router": "^5.1.16", | |||
| "@types/react-router-dom": "^5.1.8", | |||
| "sass": "^1.36.0", | |||
| "sass-loader": "^10.2.0" | |||
| } | |||
| } | |||
| @ -0,0 +1,33 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8" /> | |||
| <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
| <meta name="theme-color" content="#000000" /> | |||
| <!-- | |||
| Notice the use of %PUBLIC_URL% in the tags above. | |||
| It will be replaced with the URL of the `public` folder during the build. | |||
| Only files inside the `public` folder can be referenced from the HTML. | |||
| Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will | |||
| work correctly both with client-side routing and a non-root public URL. | |||
| Learn how to configure a non-root public URL by running `npm run build`. | |||
| --> | |||
| <title>Solstice Web UI</title> | |||
| </head> | |||
| <body> | |||
| <noscript>You need to enable JavaScript to run this app.</noscript> | |||
| <div id="root"></div> | |||
| <!-- | |||
| This HTML file is a template. | |||
| If you open it directly in the browser, you will see an empty page. | |||
| You can add webfonts, meta tags, or analytics to this file. | |||
| The build step will place the bundled scripts into the <body> tag. | |||
| To begin the development, run `npm start` or `yarn start`. | |||
| To create a production bundle, use `npm run build` or `yarn build`. | |||
| --> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,3 @@ | |||
| # https://www.robotstxt.org/robotstxt.html | |||
| User-agent: * | |||
| Disallow: | |||
| @ -0,0 +1,39 @@ | |||
| .App { | |||
| text-align: center; | |||
| } | |||
| .App-logo { | |||
| height: 40vmin; | |||
| pointer-events: none; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| .App-logo { | |||
| animation: App-logo-float infinite 3s ease-in-out; | |||
| } | |||
| } | |||
| .App-header { | |||
| min-height: 100vh; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| justify-content: center; | |||
| font-size: calc(10px + 2vmin); | |||
| } | |||
| .App-link { | |||
| color: rgb(112, 76, 182); | |||
| } | |||
| @keyframes App-logo-float { | |||
| 0% { | |||
| transform: translateY(0); | |||
| } | |||
| 50% { | |||
| transform: translateY(10px); | |||
| } | |||
| 100% { | |||
| transform: translateY(0px); | |||
| } | |||
| } | |||
| @ -0,0 +1,15 @@ | |||
| import React from 'react'; | |||
| import { render } from '@testing-library/react'; | |||
| import { Provider } from 'react-redux'; | |||
| import { store } from './app/store'; | |||
| import App from './App'; | |||
| test('renders learn react link', () => { | |||
| const { getByText } = render( | |||
| <Provider store={store}> | |||
| <App /> | |||
| </Provider> | |||
| ); | |||
| expect(getByText(/learn/i)).toBeInTheDocument(); | |||
| }); | |||
| @ -0,0 +1,58 @@ | |||
| import React from 'react'; | |||
| import logo from './logo.svg'; | |||
| import { Counter } from './features/counter/Counter'; | |||
| import './App.css'; | |||
| function App() { | |||
| return ( | |||
| <div className="App"> | |||
| <header className="App-header"> | |||
| <img src={logo} className="App-logo" alt="logo" /> | |||
| <Counter /> | |||
| <p> | |||
| Edit <code>src/App.tsx</code> and save to reload. | |||
| </p> | |||
| <span> | |||
| <span>Learn </span> | |||
| <a | |||
| className="App-link" | |||
| href="https://reactjs.org/" | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| React | |||
| </a> | |||
| <span>, </span> | |||
| <a | |||
| className="App-link" | |||
| href="https://redux.js.org/" | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| Redux | |||
| </a> | |||
| <span>, </span> | |||
| <a | |||
| className="App-link" | |||
| href="https://redux-toolkit.js.org/" | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| Redux Toolkit | |||
| </a> | |||
| ,<span> and </span> | |||
| <a | |||
| className="App-link" | |||
| href="https://react-redux.js.org/" | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| React Redux | |||
| </a> | |||
| </span> | |||
| </header> | |||
| </div> | |||
| ); | |||
| } | |||
| export default App; | |||
| @ -0,0 +1,6 @@ | |||
| import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; | |||
| import type { RootState, AppDispatch } from './store'; | |||
| // Use throughout your app instead of plain `useDispatch` and `useSelector` | |||
| export const useAppDispatch = () => useDispatch<AppDispatch>(); | |||
| export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; | |||
| @ -0,0 +1,34 @@ | |||
| import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; | |||
| import counterReducer from "../features/counter/counterSlice"; | |||
| // TODO: Rework these to use a slice, otherwise redux complains about | |||
| // non-serializable Immutable.js types. | |||
| // | |||
| // import login from "../reducers/login"; | |||
| // import rooms from "../reducers/rooms"; | |||
| // import users from "../reducers/users"; | |||
| // | |||
| import socketMiddleware from "../modules/websocket/middleware"; | |||
| import socketReducer from "../modules/websocket/slice"; | |||
| export const store = configureStore({ | |||
| reducer: { | |||
| counter: counterReducer, | |||
| //login, | |||
| //rooms, | |||
| socket: socketReducer, | |||
| //users, | |||
| }, | |||
| middleware: (getDefaultMiddleware) => ( | |||
| getDefaultMiddleware().concat(socketMiddleware) | |||
| ) | |||
| }); | |||
| export type AppDispatch = typeof store.dispatch; | |||
| export type RootState = ReturnType<typeof store.getState>; | |||
| export type AppThunk<ReturnType = void> = ThunkAction< | |||
| ReturnType, | |||
| RootState, | |||
| unknown, | |||
| Action<string> | |||
| >; | |||
| @ -1,45 +1,58 @@ | |||
| import React, {PropTypes} from "react"; | |||
| import {reduxForm} from "redux-form"; | |||
| import ImmutablePropTypes from "react-immutable-proptypes"; | |||
| import React from "react"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { Form, Field } from "react-final-form"; | |||
| import SocketStatusPane from "./SocketStatusPane"; | |||
| import { SocketRecord } from "../reducers/socket"; | |||
| import { STATE_CLOSED } from "../constants/socket"; | |||
| import { | |||
| SocketSliceState, | |||
| SocketState, | |||
| socketOpen | |||
| } from "../modules/websocket/slice"; | |||
| const ConnectForm = (props) => { | |||
| const { fields: { url }, handleSubmit, socket, actions } = props; | |||
| interface Props { | |||
| socket: SocketSliceState, | |||
| }; | |||
| const onSubmit = handleSubmit((values) => { | |||
| return actions.socket.open(values.url, actions.socketHandlers); | |||
| }); | |||
| const ConnectForm: React.FC<Props> = ({ socket }) => { | |||
| const dispatch = useDispatch(); | |||
| const isSocketClosed = socket.state === STATE_CLOSED; | |||
| const onSubmit = ({ url }) => { | |||
| dispatch(socketOpen(url)); | |||
| }; | |||
| return ( | |||
| <div id="connect-form"> | |||
| <h2>Connect to a solstice client</h2> | |||
| <form onSubmit={onSubmit}> | |||
| <input type="url" defaultValue="ws://localhost:2244" {...url} | |||
| required pattern="wss?://.+"/> | |||
| <button type="submit" disabled={!isSocketClosed}> | |||
| Connect | |||
| </button> | |||
| </form> | |||
| <SocketStatusPane | |||
| state={socket.state} | |||
| url={socket.url} | |||
| /> | |||
| </div> | |||
| ); | |||
| }; | |||
| const isSocketClosed = socket.state === SocketState.Closed; | |||
| ConnectForm.propTypes = { | |||
| fields: PropTypes.object.isRequired, | |||
| handleSubmit: PropTypes.func.isRequired, | |||
| socket: ImmutablePropTypes.record.isRequired, | |||
| actions: PropTypes.object.isRequired | |||
| return ( | |||
| <div id="connect-form"> | |||
| <h2>Connect to a solstice client</h2> | |||
| <Form | |||
| onSubmit={onSubmit} | |||
| render={({ handleSubmit, submitting }) => ( | |||
| <form onSubmit={handleSubmit}> | |||
| <Field | |||
| name="url" | |||
| component="input" | |||
| type="url" | |||
| defaultValue="ws://localhost:2244" | |||
| required | |||
| pattern="wss?://.+" | |||
| /> | |||
| <button | |||
| type="submit" | |||
| disabled={submitting || !isSocketClosed}> | |||
| Connect | |||
| </button> | |||
| </form> | |||
| )} | |||
| /> | |||
| <SocketStatusPane | |||
| state={socket.state} | |||
| url={socket.url} | |||
| /> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default reduxForm({ | |||
| form: "connect", | |||
| fields: ["url"] | |||
| })(ConnectForm); | |||
| export default ConnectForm; | |||
| @ -1,21 +1,51 @@ | |||
| import React, {PropTypes} from "react"; | |||
| import React from "react"; | |||
| import { connect } from "react-redux"; | |||
| import { Switch, Redirect, Route, useLocation } from "react-router-dom"; | |||
| import Header from "./Header"; | |||
| import { STATE_OPEN } from "../constants/socket"; | |||
| import ConnectPage from "../containers/ConnectPage"; | |||
| import Footer from "../containers/Footer"; | |||
| import { SocketRecord } from "../reducers/socket"; | |||
| const ConnectedApp: React.FC<SocketRecord> = ({ socket, children }) => { | |||
| const location = useLocation(); | |||
| if (socket.state !== STATE_OPEN) { | |||
| return ( | |||
| <Redirect | |||
| to={{ | |||
| pathname: "/connect", | |||
| state: { from: location }, | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| const SolsticeApp = ({ children }) => ( | |||
| return ( | |||
| <div id="solstice-app"> | |||
| <Header /> | |||
| <main> | |||
| {children} | |||
| </main> | |||
| <Footer /> | |||
| <Header /> | |||
| <main> | |||
| {children} | |||
| </main> | |||
| <Footer /> | |||
| </div> | |||
| ); | |||
| }; | |||
| const SolsticeApp = ({ socket, children }) => ( | |||
| <Switch> | |||
| <Route path="/connect"> | |||
| <ConnectPage /> | |||
| </Route> | |||
| <Route path="/"> | |||
| <ConnectedApp socket={socket}> | |||
| {children} | |||
| </ConnectedApp> | |||
| </Route> | |||
| </Switch> | |||
| ); | |||
| SolsticeApp.propTypes = { | |||
| children: PropTypes.element | |||
| }; | |||
| const mapStateToProps = ({ socket }) => ({ socket }); | |||
| export default SolsticeApp; | |||
| export default connect(mapStateToProps)(SolsticeApp); | |||
| @ -1,34 +1,34 @@ | |||
| import React, { PropTypes } from "react"; | |||
| import React from "react"; | |||
| import { connect } from "react-redux"; | |||
| import ImmutablePropTypes from "react-immutable-proptypes"; | |||
| import { SocketSliceState } from "../modules/websocket/slice"; | |||
| import LoginStatusPane from "../components/LoginStatusPane"; | |||
| import SocketStatusPane from "../components/SocketStatusPane"; | |||
| const Footer = ({ login, socket }) => { | |||
| interface Props { | |||
| //login: any, | |||
| socket: SocketSliceState, | |||
| }; | |||
| const Footer = ({ socket }) => { | |||
| return ( | |||
| <footer> | |||
| <SocketStatusPane | |||
| state={socket.state} | |||
| url={socket.url} | |||
| /> | |||
| {/* | |||
| <LoginStatusPane | |||
| status={login.status} | |||
| username={login.username} | |||
| motd={login.motd} | |||
| reason={login.reason} | |||
| /> | |||
| */} | |||
| </footer> | |||
| ); | |||
| }; | |||
| Footer.propTypes = { | |||
| login: ImmutablePropTypes.record.isRequired, | |||
| socket: ImmutablePropTypes.record.isRequired | |||
| }; | |||
| const mapStateToProps = ({ socket, login }) => ({ socket, login }); | |||
| const mapStateToProps = ({ socket }) => ({ socket }); | |||
| export default connect( | |||
| mapStateToProps | |||
| )(Footer); | |||
| export default connect(mapStateToProps)(Footer); | |||
| @ -1,35 +0,0 @@ | |||
| import React, { PropTypes } from 'react'; | |||
| import { Route, IndexRoute } from 'react-router'; | |||
| import ConnectPage from "./containers/ConnectPage"; | |||
| import RoomsPane from "./containers/RoomsPane"; | |||
| import UsersPane from "./containers/UsersPane"; | |||
| import SolsticeApp from "./components/SolsticeApp"; | |||
| import { STATE_OPEN } from "./constants/socket"; | |||
| import { LOGIN_STATUS_SUCCESS } from "./constants/login"; | |||
| const createRoutes = (store) => { | |||
| const requireLoggedIn = (nextState, replaceState) => { | |||
| let { socket, login } = store.getState(); | |||
| if (socket.state !== STATE_OPEN || | |||
| login.status !== LOGIN_STATUS_SUCCESS) | |||
| { | |||
| replaceState({}, "/"); | |||
| } | |||
| }; | |||
| return ( | |||
| <Route path="/"> | |||
| <IndexRoute component={ConnectPage} /> | |||
| <Route path="app" onEnter={requireLoggedIn} component={SolsticeApp}> | |||
| <Route path="rooms(/:roomNameHash)" component={RoomsPane} /> | |||
| <Route path="users(/:userNameHash)" component={UsersPane} /> | |||
| </Route> | |||
| </Route> | |||
| ); | |||
| }; | |||
| export default createRoutes; | |||
| @ -0,0 +1,79 @@ | |||
| .row { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| } | |||
| .row > button { | |||
| margin-left: 4px; | |||
| margin-right: 8px; | |||
| } | |||
| .row:not(:last-child) { | |||
| margin-bottom: 16px; | |||
| } | |||
| .value { | |||
| font-size: 78px; | |||
| padding-left: 16px; | |||
| padding-right: 16px; | |||
| margin-top: 2px; | |||
| font-family: 'Courier New', Courier, monospace; | |||
| } | |||
| .button { | |||
| appearance: none; | |||
| background: none; | |||
| font-size: 32px; | |||
| padding-left: 12px; | |||
| padding-right: 12px; | |||
| outline: none; | |||
| border: 2px solid transparent; | |||
| color: rgb(112, 76, 182); | |||
| padding-bottom: 4px; | |||
| cursor: pointer; | |||
| background-color: rgba(112, 76, 182, 0.1); | |||
| border-radius: 2px; | |||
| transition: all 0.15s; | |||
| } | |||
| .textbox { | |||
| font-size: 32px; | |||
| padding: 2px; | |||
| width: 64px; | |||
| text-align: center; | |||
| margin-right: 4px; | |||
| } | |||
| .button:hover, | |||
| .button:focus { | |||
| border: 2px solid rgba(112, 76, 182, 0.4); | |||
| } | |||
| .button:active { | |||
| background-color: rgba(112, 76, 182, 0.2); | |||
| } | |||
| .asyncButton { | |||
| composes: button; | |||
| position: relative; | |||
| } | |||
| .asyncButton:after { | |||
| content: ''; | |||
| background-color: rgba(112, 76, 182, 0.15); | |||
| display: block; | |||
| position: absolute; | |||
| width: 100%; | |||
| height: 100%; | |||
| left: 0; | |||
| top: 0; | |||
| opacity: 0; | |||
| transition: width 1s linear, opacity 0.5s ease 1s; | |||
| } | |||
| .asyncButton:active:after { | |||
| width: 0%; | |||
| opacity: 1; | |||
| transition: 0s; | |||
| } | |||
| @ -0,0 +1,68 @@ | |||
| import React, { useState } from 'react'; | |||
| import { useAppSelector, useAppDispatch } from '../../app/hooks'; | |||
| import { | |||
| decrement, | |||
| increment, | |||
| incrementByAmount, | |||
| incrementAsync, | |||
| incrementIfOdd, | |||
| selectCount, | |||
| } from './counterSlice'; | |||
| import styles from './Counter.module.css'; | |||
| export function Counter() { | |||
| const count = useAppSelector(selectCount); | |||
| const dispatch = useAppDispatch(); | |||
| const [incrementAmount, setIncrementAmount] = useState('2'); | |||
| const incrementValue = Number(incrementAmount) || 0; | |||
| return ( | |||
| <div> | |||
| <div className={styles.row}> | |||
| <button | |||
| className={styles.button} | |||
| aria-label="Decrement value" | |||
| onClick={() => dispatch(decrement())} | |||
| > | |||
| - | |||
| </button> | |||
| <span className={styles.value}>{count}</span> | |||
| <button | |||
| className={styles.button} | |||
| aria-label="Increment value" | |||
| onClick={() => dispatch(increment())} | |||
| > | |||
| + | |||
| </button> | |||
| </div> | |||
| <div className={styles.row}> | |||
| <input | |||
| className={styles.textbox} | |||
| aria-label="Set increment amount" | |||
| value={incrementAmount} | |||
| onChange={(e) => setIncrementAmount(e.target.value)} | |||
| /> | |||
| <button | |||
| className={styles.button} | |||
| onClick={() => dispatch(incrementByAmount(incrementValue))} | |||
| > | |||
| Add Amount | |||
| </button> | |||
| <button | |||
| className={styles.asyncButton} | |||
| onClick={() => dispatch(incrementAsync(incrementValue))} | |||
| > | |||
| Add Async | |||
| </button> | |||
| <button | |||
| className={styles.button} | |||
| onClick={() => dispatch(incrementIfOdd(incrementValue))} | |||
| > | |||
| Add If Odd | |||
| </button> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| @ -0,0 +1,6 @@ | |||
| // A mock function to mimic making an async request for data | |||
| export function fetchCount(amount = 1) { | |||
| return new Promise<{ data: number }>((resolve) => | |||
| setTimeout(() => resolve({ data: amount }), 500) | |||
| ); | |||
| } | |||
| @ -0,0 +1,34 @@ | |||
| import counterReducer, { | |||
| CounterState, | |||
| increment, | |||
| decrement, | |||
| incrementByAmount, | |||
| } from './counterSlice'; | |||
| describe('counter reducer', () => { | |||
| const initialState: CounterState = { | |||
| value: 3, | |||
| status: 'idle', | |||
| }; | |||
| it('should handle initial state', () => { | |||
| expect(counterReducer(undefined, { type: 'unknown' })).toEqual({ | |||
| value: 0, | |||
| status: 'idle', | |||
| }); | |||
| }); | |||
| it('should handle increment', () => { | |||
| const actual = counterReducer(initialState, increment()); | |||
| expect(actual.value).toEqual(4); | |||
| }); | |||
| it('should handle decrement', () => { | |||
| const actual = counterReducer(initialState, decrement()); | |||
| expect(actual.value).toEqual(2); | |||
| }); | |||
| it('should handle incrementByAmount', () => { | |||
| const actual = counterReducer(initialState, incrementByAmount(2)); | |||
| expect(actual.value).toEqual(5); | |||
| }); | |||
| }); | |||
| @ -0,0 +1,82 @@ | |||
| import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; | |||
| import { RootState, AppThunk } from '../../app/store'; | |||
| import { fetchCount } from './counterAPI'; | |||
| export interface CounterState { | |||
| value: number; | |||
| status: 'idle' | 'loading' | 'failed'; | |||
| } | |||
| const initialState: CounterState = { | |||
| value: 0, | |||
| status: 'idle', | |||
| }; | |||
| // The function below is called a thunk and allows us to perform async logic. It | |||
| // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This | |||
| // will call the thunk with the `dispatch` function as the first argument. Async | |||
| // code can then be executed and other actions can be dispatched. Thunks are | |||
| // typically used to make async requests. | |||
| export const incrementAsync = createAsyncThunk( | |||
| 'counter/fetchCount', | |||
| async (amount: number) => { | |||
| const response = await fetchCount(amount); | |||
| // The value we return becomes the `fulfilled` action payload | |||
| return response.data; | |||
| } | |||
| ); | |||
| export const counterSlice = createSlice({ | |||
| name: 'counter', | |||
| initialState, | |||
| // The `reducers` field lets us define reducers and generate associated actions | |||
| reducers: { | |||
| increment: (state) => { | |||
| // Redux Toolkit allows us to write "mutating" logic in reducers. It | |||
| // doesn't actually mutate the state because it uses the Immer library, | |||
| // which detects changes to a "draft state" and produces a brand new | |||
| // immutable state based off those changes | |||
| state.value += 1; | |||
| }, | |||
| decrement: (state) => { | |||
| state.value -= 1; | |||
| }, | |||
| // Use the PayloadAction type to declare the contents of `action.payload` | |||
| incrementByAmount: (state, action: PayloadAction<number>) => { | |||
| state.value += action.payload; | |||
| }, | |||
| }, | |||
| // The `extraReducers` field lets the slice handle actions defined elsewhere, | |||
| // including actions generated by createAsyncThunk or in other slices. | |||
| extraReducers: (builder) => { | |||
| builder | |||
| .addCase(incrementAsync.pending, (state) => { | |||
| state.status = 'loading'; | |||
| }) | |||
| .addCase(incrementAsync.fulfilled, (state, action) => { | |||
| state.status = 'idle'; | |||
| state.value += action.payload; | |||
| }); | |||
| }, | |||
| }); | |||
| export const { increment, decrement, incrementByAmount } = counterSlice.actions; | |||
| // The function below is called a selector and allows us to select a value from | |||
| // the state. Selectors can also be defined inline where they're used instead of | |||
| // in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` | |||
| export const selectCount = (state: RootState) => state.counter.value; | |||
| // We can also write thunks by hand, which may contain both sync and async logic. | |||
| // Here's an example of conditionally dispatching actions based on current state. | |||
| export const incrementIfOdd = (amount: number): AppThunk => ( | |||
| dispatch, | |||
| getState | |||
| ) => { | |||
| const currentValue = selectCount(getState()); | |||
| if (currentValue % 2 === 1) { | |||
| dispatch(incrementByAmount(amount)); | |||
| } | |||
| }; | |||
| export default counterSlice.reducer; | |||
| @ -1,11 +0,0 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
| <title>Solstice Web UI</title> | |||
| </head> | |||
| <body> | |||
| <div id="app"></div> | |||
| <script src="bundle.js"></script> | |||
| </body> | |||
| </html> | |||
| @ -1,20 +0,0 @@ | |||
| import React from 'react'; | |||
| import {render} from 'react-dom'; | |||
| import { Provider } from 'react-redux'; | |||
| import { Router, hashHistory } from "react-router"; | |||
| import configureStore from './store/configureStore'; | |||
| import createRoutes from "./createRoutes"; | |||
| import './styles/styles.scss'; //Yep, that's right. You can import SASS/CSS files too! Webpack will run the associated loader and plug this into the page. | |||
| const store = configureStore(); | |||
| const routes = createRoutes(store); | |||
| render( | |||
| <Provider store={store}> | |||
| <Router history={hashHistory}> | |||
| {routes} | |||
| </Router> | |||
| </Provider>, document.getElementById('app') | |||
| ); | |||
| @ -0,0 +1,21 @@ | |||
| import React from 'react'; | |||
| import ReactDOM from 'react-dom'; | |||
| import { Provider } from 'react-redux'; | |||
| import { BrowserRouter as Router } from "react-router-dom"; | |||
| import './styles/styles.scss'; | |||
| import { store } from "./app/store"; | |||
| import SolsticeApp from "./components/SolsticeApp"; | |||
| ReactDOM.render( | |||
| <React.StrictMode> | |||
| <Provider store={store}> | |||
| <Router> | |||
| <SolsticeApp /> | |||
| </Router> | |||
| </Provider> | |||
| </React.StrictMode>, | |||
| document.getElementById('root') | |||
| ); | |||
| @ -0,0 +1,5 @@ | |||
| export const wsConnect = host => ({ type: 'WS_CONNECT', host }); | |||
| export const wsConnecting = host => ({ type: 'WS_CONNECTING', host }); | |||
| export const wsConnected = host => ({ type: 'WS_CONNECTED', host }); | |||
| export const wsDisconnect = () => ({ type: 'WS_DISCONNECT' }); | |||
| export const wsDisconnected = () => ({ type: 'WS_DISCONNECTED' }); | |||
| @ -0,0 +1,82 @@ | |||
| // A middleware that allows controlling a singleton websocket via redux actions. | |||
| // | |||
| // Largely copied from the excellent tutorial by Lina Rudashevski: | |||
| // https://dev.to/aduranil/how-to-use-websockets-with-redux-a-step-by-step-guide-to-writing-understanding-connecting-socket-middleware-to-your-project-km3 | |||
| import { Middleware } from "redux"; | |||
| import { | |||
| socketOpen, | |||
| socketOpened, | |||
| socketClose, | |||
| socketClosed, | |||
| socketSendMessage, | |||
| socketReceiveMessage, | |||
| } from "./slice"; | |||
| import { RootState } from "../app/store"; | |||
| import { SOCKET_RECEIVE_MESSAGE } from "../../constants/ActionTypes"; | |||
| // The WebSocket singleton. | |||
| let socket: WebSocket | null = null; | |||
| const onOpen = dispatch => event => { | |||
| console.log('Websocket open', event.target.url); | |||
| dispatch(socketOpened(event.target.url)); | |||
| }; | |||
| const onClose = dispatch => () => { | |||
| dispatch(socketClosed()); | |||
| }; | |||
| const onMessage = dispatch => event => { | |||
| console.log(`Websocket received message: ${event.data}`); | |||
| // TODO: dispatch different actions based on payload type. | |||
| const action = { type: SOCKET_RECEIVE_MESSAGE }; | |||
| try { | |||
| const { variant, fields: [data] } = JSON.parse(event.data); | |||
| if (typeof variant === "undefined") { | |||
| throw new Error('Missing "variant" field in control response'); | |||
| } | |||
| action.payload = { variant, data }; | |||
| } catch (err) { | |||
| action.error = true; | |||
| action.payload = err; | |||
| } | |||
| dispatch(action); | |||
| }; | |||
| // See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware | |||
| const middleware: Middleware<{}, RootState> = storeApi => next => action => { | |||
| if (socketOpen.match(action)) { | |||
| if (socket !== null) { | |||
| socket.close(); | |||
| } | |||
| // Connect to the remote host. | |||
| socket = new WebSocket(action.payload); | |||
| // Bind websocket handlers. | |||
| socket.onmessage = onMessage(storeApi.dispatch); | |||
| socket.onclose = onClose(storeApi.dispatch); | |||
| socket.onopen = onOpen(storeApi.dispatch); | |||
| } else if (socketClose.match(action)) { | |||
| if (socket !== null) { | |||
| socket.close(); | |||
| } | |||
| socket = null; | |||
| console.log('Websocket closed.'); | |||
| } else if (socketSendMessage.match(action)) { | |||
| console.log('Websocket sending message', action.payload); | |||
| socket.send(JSON.stringify(action.payload)); | |||
| } | |||
| return next(action); | |||
| }; | |||
| export default middleware; | |||
| @ -0,0 +1,47 @@ | |||
| import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit'; | |||
| import { RootState, AppThunk } from '../../app/store'; | |||
| export enum SocketState { | |||
| Opening, | |||
| Open, | |||
| Closing, | |||
| Closed, | |||
| }; | |||
| export interface SocketSliceState { | |||
| state: SocketState, | |||
| url: string | null, | |||
| }; | |||
| const initialState: SocketSliceState = { | |||
| state: SocketState.Closed, | |||
| url: null, | |||
| }; | |||
| export const socketSlice = createSlice({ | |||
| name: 'socket', | |||
| initialState, | |||
| reducers: { | |||
| socketOpen: (state, action: PayloadAction<string>) => { | |||
| state.state = SocketState.Opening; | |||
| state.url = action.payload; | |||
| }, | |||
| socketOpened: (state, action: PayoadAction<string>) => { | |||
| state.state = SocketState.Open; | |||
| state.url = action.payload; | |||
| }, | |||
| socketClose: (state) => { | |||
| state.state = SocketState.Closing; | |||
| }, | |||
| socketClosed: (state) => { | |||
| state.state = SocketState.Closed; | |||
| }, | |||
| }, | |||
| }); | |||
| export const { socketOpen, socketOpened, socketClose, socketClosed } = | |||
| socketSlice.actions; | |||
| export const socketSendMessage = createAction<object>("socketSendMessage"); | |||
| export default socketSlice.reducer; | |||
| @ -0,0 +1 @@ | |||
| /// <reference types="react-scripts" /> | |||
| @ -0,0 +1,5 @@ | |||
| // jest-dom adds custom jest matchers for asserting on DOM nodes. | |||
| // allows you to do things like: | |||
| // expect(element).toHaveTextContent(/react/i) | |||
| // learn more: https://github.com/testing-library/jest-dom | |||
| import '@testing-library/jest-dom/extend-expect'; | |||
| @ -1,26 +0,0 @@ | |||
| //This file merely configures the store for hot reloading. | |||
| //This boilerplate file is likely to be the same for each project that uses Redux. | |||
| //With Redux, the actual stores are in /reducers. | |||
| import { createStore, compose } from "redux"; | |||
| import rootReducer from "../reducers"; | |||
| export default function configureStore(initialState, storeEnhancer) { | |||
| if (window.devToolsExtension) { | |||
| // Enable Redux devtools if the extension is installed in developer's | |||
| // browser. | |||
| storeEnhancer = compose(storeEnhancer, window.devToolsExtension()); | |||
| } | |||
| const store = createStore(rootReducer, initialState, storeEnhancer); | |||
| if (module.hot) { | |||
| // Enable Webpack hot module replacement for reducers | |||
| module.hot.accept('../reducers', () => { | |||
| const nextReducer = require('../reducers'); | |||
| store.replaceReducer(nextReducer); | |||
| }); | |||
| } | |||
| return store; | |||
| } | |||
| @ -1,20 +0,0 @@ | |||
| import { applyMiddleware } from "redux"; | |||
| import thunk from "redux-thunk"; | |||
| import promise from "redux-promise"; | |||
| import createLogger from "redux-logger"; | |||
| let configureStore; | |||
| if (process.env.NODE_ENV === 'production') { | |||
| configureStore = require('./configureStore.prod').default; | |||
| } else { | |||
| configureStore = require('./configureStore.dev').default; | |||
| } | |||
| export default () => { | |||
| const logger = createLogger(); | |||
| const initialState = undefined; | |||
| return configureStore( | |||
| initialState, | |||
| applyMiddleware(thunk, promise, logger) | |||
| ); | |||
| }; | |||
| @ -1,6 +0,0 @@ | |||
| import { createStore } from 'redux'; | |||
| import rootReducer from '../reducers'; | |||
| export default function configureStore(initialState, enhancer) { | |||
| return createStore(rootReducer, initialState, enhancer); | |||
| } | |||
| @ -1,45 +0,0 @@ | |||
| // More info on Webpack's Node API here: https://webpack.github.io/docs/node.js-api.html | |||
| // Allowing console calls below since this is a build file. | |||
| /*eslint-disable no-console */ | |||
| import webpack from 'webpack'; | |||
| import webpackConfigBuilder from '../webpack.config'; | |||
| import colors from 'colors'; | |||
| import { argv as args } from 'yargs'; | |||
| process.env.NODE_ENV = 'production'; // this assures React is built in prod mode and that the Babel dev config doesn't apply. | |||
| const webpackConfig = webpackConfigBuilder(process.env.NODE_ENV); | |||
| webpack(webpackConfig).run((err, stats) => { | |||
| const inSilentMode = args.s; // set to true when -s is passed on the command | |||
| if (!inSilentMode) { | |||
| console.log('Generating minified bundle for production use via Webpack...'.bold.blue); | |||
| } | |||
| if (err) { // so a fatal error occurred. Stop here. | |||
| console.log(err.bold.red); | |||
| return 1; | |||
| } | |||
| const jsonStats = stats.toJson(); | |||
| if (jsonStats.hasErrors) { | |||
| return jsonStats.errors.map(error => console.log(error.red)); | |||
| } | |||
| if (jsonStats.hasWarnings && !inSilentMode) { | |||
| console.log('Webpack generated the following warnings: '.bold.yellow); | |||
| jsonStats.warnings.map(warning => console.log(warning.yellow)); | |||
| } | |||
| if (!inSilentMode) { | |||
| console.log(`Webpack stats: ${stats}`); | |||
| } | |||
| // if we got this far, the build succeeded. | |||
| console.log('Your app has been compiled in production mode and written to /dist. It\'s ready to roll!'.green.bold); | |||
| return 0; | |||
| }); | |||
| @ -1,45 +0,0 @@ | |||
| // This script copies src/index.html into /dist/index.html | |||
| // and adds TrackJS error tracking code for use in production | |||
| // when useTrackJs is set to true below and a trackJsToken is provided. | |||
| // This is a good example of using Node and cheerio to do a simple file transformation. | |||
| // In this case, the transformation is useful since we only want to track errors in the built production code. | |||
| // Allowing console calls below since this is a build file. | |||
| /*eslint-disable no-console */ | |||
| import fs from 'fs'; | |||
| import colors from 'colors'; | |||
| import cheerio from 'cheerio'; | |||
| const useTrackJs = true; // If you choose not to use TrackJS, just set this to false and the build warning will go away. | |||
| const trackJsToken = ''; // If you choose to use TrackJS, insert your unique token here. To get a token, go to https://trackjs.com | |||
| fs.readFile('src/index.html', 'utf8', (err, markup) => { | |||
| if (err) { | |||
| return console.log(err); | |||
| } | |||
| const $ = cheerio.load(markup); | |||
| // since a separate spreadsheet is only utilized for the production build, need to dynamically add this here. | |||
| $('head').prepend('<link rel="stylesheet" href="styles.css">'); | |||
| if (useTrackJs) { | |||
| if (trackJsToken) { | |||
| const trackJsCode = `<!-- BEGIN TRACKJS Note: This should be the first <script> on the page per https://my.trackjs.com/install --><script>window._trackJs = { token: '${trackJsToken}' };</script><script src=https://d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js></script><!-- END TRACKJS -->`; | |||
| $('head').prepend(trackJsCode); // add TrackJS tracking code to the top of <head> | |||
| } else { | |||
| console.log('To track JavaScript errors, sign up for a free trial at TrackJS.com and enter your token in /tools/build.html on line 10.'.yellow); | |||
| } | |||
| } | |||
| fs.writeFile('dist/index.html', $.html(), 'utf8', function (err) { | |||
| if (err) { | |||
| return console.log(err); | |||
| } | |||
| }); | |||
| console.log('index.html written to /dist'.green); | |||
| }); | |||
| @ -1,19 +0,0 @@ | |||
| // This file configures a web server for testing the production build | |||
| // on your local machine. | |||
| import browserSync from 'browser-sync'; | |||
| // Run Browsersync | |||
| browserSync({ | |||
| port: 3000, | |||
| ui: { | |||
| port: 3001 | |||
| }, | |||
| server: { | |||
| baseDir: 'dist' | |||
| }, | |||
| files: [ | |||
| 'src/*.html' | |||
| ] | |||
| }); | |||
| @ -1,44 +0,0 @@ | |||
| // This file configures the development web server | |||
| // which supports hot reloading and synchronized testing. | |||
| // Require Browsersync along with webpack and middleware for it | |||
| import browserSync from 'browser-sync'; | |||
| import webpack from 'webpack'; | |||
| import webpackDevMiddleware from 'webpack-dev-middleware'; | |||
| import webpackHotMiddleware from 'webpack-hot-middleware'; | |||
| import webpackConfigBuilder from '../webpack.config'; | |||
| const webpackConfig = webpackConfigBuilder('development'); | |||
| const bundler = webpack(webpackConfig); | |||
| // Run Browsersync and use middleware for Hot Module Replacement | |||
| browserSync({ | |||
| server: { | |||
| baseDir: 'src', | |||
| middleware: [ | |||
| webpackDevMiddleware(bundler, { | |||
| // Dev middleware can't access config, so we provide publicPath | |||
| publicPath: webpackConfig.output.publicPath, | |||
| // pretty colored output | |||
| stats: { colors: true }, | |||
| // Set to false to display a list of each file that is being bundled. | |||
| noInfo: true | |||
| // for other settings see | |||
| // http://webpack.github.io/docs/webpack-dev-middleware.html | |||
| }), | |||
| // bundler should be the same as above | |||
| webpackHotMiddleware(bundler) | |||
| ] | |||
| }, | |||
| // no need to watch '*.js' here, webpack will take care of it for us, | |||
| // including full page reloads if HMR won't work | |||
| files: [ | |||
| 'src/*.html' | |||
| ] | |||
| }); | |||
| @ -0,0 +1,26 @@ | |||
| { | |||
| "compilerOptions": { | |||
| "target": "es5", | |||
| "lib": [ | |||
| "dom", | |||
| "dom.iterable", | |||
| "esnext" | |||
| ], | |||
| "allowJs": true, | |||
| "skipLibCheck": true, | |||
| "esModuleInterop": true, | |||
| "allowSyntheticDefaultImports": true, | |||
| "strict": true, | |||
| "forceConsistentCasingInFileNames": true, | |||
| "noFallthroughCasesInSwitch": true, | |||
| "module": "esnext", | |||
| "moduleResolution": "node", | |||
| "resolveJsonModule": true, | |||
| "isolatedModules": true, | |||
| "noEmit": true, | |||
| "jsx": "react-jsx" | |||
| }, | |||
| "include": [ | |||
| "src" | |||
| ] | |||
| } | |||
| @ -1,83 +0,0 @@ | |||
| // For info about this file refer to webpack and webpack-hot-middleware documentation | |||
| // Rather than having hard coded webpack.config.js for each environment, this | |||
| // file generates a webpack config for the environment passed to the getConfig method. | |||
| import webpack from 'webpack'; | |||
| import path from 'path'; | |||
| import ExtractTextPlugin from 'extract-text-webpack-plugin'; | |||
| const developmentEnvironment = 'development' ; | |||
| const productionEnvironment = 'production'; | |||
| const testEnvironment = 'test'; | |||
| const getPlugins = function (env) { | |||
| const GLOBALS = { | |||
| 'process.env.NODE_ENV': JSON.stringify(env), | |||
| __DEV__: env === developmentEnvironment | |||
| }; | |||
| const plugins = [ | |||
| new webpack.optimize.OccurenceOrderPlugin(), | |||
| new webpack.DefinePlugin(GLOBALS) //Tells React to build in prod mode. https://facebook.github.io/react/downloads.html | |||
| ]; | |||
| switch (env) { | |||
| case productionEnvironment: | |||
| plugins.push(new ExtractTextPlugin('styles.css')); | |||
| plugins.push(new webpack.optimize.DedupePlugin()); | |||
| plugins.push(new webpack.optimize.UglifyJsPlugin()); | |||
| break; | |||
| case developmentEnvironment: | |||
| plugins.push(new webpack.HotModuleReplacementPlugin()); | |||
| plugins.push(new webpack.NoErrorsPlugin()); | |||
| break; | |||
| } | |||
| return plugins; | |||
| }; | |||
| const getEntry = function (env) { | |||
| const entry = []; | |||
| if (env === developmentEnvironment ) { // only want hot reloading when in dev. | |||
| entry.push('webpack-hot-middleware/client'); | |||
| } | |||
| entry.push('./src/index'); | |||
| return entry; | |||
| }; | |||
| const getLoaders = function (env) { | |||
| const loaders = [{ test: /\.js$/, include: path.join(__dirname, 'src'), loaders: ['babel', 'eslint'] }]; | |||
| if (env === productionEnvironment ) { | |||
| // generate separate physical stylesheet for production build using ExtractTextPlugin. This provides separate caching and avoids a flash of unstyled content on load. | |||
| loaders.push({test: /(\.css|\.scss)$/, loader: ExtractTextPlugin.extract("css?sourceMap!sass?sourceMap")}); | |||
| } else { | |||
| loaders.push({test: /(\.css|\.scss)$/, loaders: ['style', 'css?sourceMap', 'sass?sourceMap']}); | |||
| } | |||
| return loaders; | |||
| }; | |||
| function getConfig(env) { | |||
| return { | |||
| debug: true, | |||
| devtool: env === productionEnvironment ? 'source-map' : 'cheap-module-eval-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool | |||
| noInfo: true, // set to false to see a list of every file being bundled. | |||
| entry: getEntry(env), | |||
| target: env === testEnvironment ? 'node' : 'web', // necessary per https://webpack.github.io/docs/testing.html#compile-and-test | |||
| output: { | |||
| path: __dirname + '/dist', // Note: Physical files are only output by the production build task `npm run build`. | |||
| publicPath: '', | |||
| filename: 'bundle.js' | |||
| }, | |||
| plugins: getPlugins(env), | |||
| module: { | |||
| loaders: getLoaders(env) | |||
| } | |||
| }; | |||
| } | |||
| export default getConfig; | |||