├── uploads
└── .gitkeep
├── src
├── home
│ ├── style.scss
│ └── Home.tsx
├── counter
│ ├── style.scss
│ ├── __tests__
│ │ ├── module.spec.ts
│ │ ├── Counter.spec.tsx
│ │ └── ActionDispatcher.spec.ts
│ ├── Counter.tsx
│ ├── Container.tsx
│ └── module.ts
├── Variables.ts
├── NotFound.tsx
├── __tests__
│ └── enzyme-util.ts
├── store.ts
├── Index.tsx
├── upload
│ ├── FileUploadSample.tsx
│ ├── module.ts
│ └── Container.tsx
├── Routes.tsx
└── dnd
│ ├── Container.tsx
│ └── Card.tsx
├── config
├── variables
│ ├── dev.js
│ └── prod.js
├── dev-server.js
├── webpack.config.prod.js
└── webpack.config.dev.js
├── .gitignore
├── public
└── images
│ └── favicon.ico
├── moveToPublic.sh
├── prettier.config.js
├── index.template.ejs
├── .circleci
└── config.yml
├── .eslintrc.js
├── tsconfig.json
├── Readme.md
├── LICENSE.txt
├── karma.conf.js
└── package.json
/uploads/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/home/style.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | color: brown;
3 | }
--------------------------------------------------------------------------------
/src/counter/style.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | color: blue;
3 | }
--------------------------------------------------------------------------------
/config/variables/dev.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ENV_PRODUCTION: false,
3 | }
4 |
--------------------------------------------------------------------------------
/config/variables/prod.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ENV_PRODUCTION: true,
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .idea/
3 | public/bundle/
4 | public/libs/
5 | public/index.html
6 | uploads/
--------------------------------------------------------------------------------
/src/Variables.ts:
--------------------------------------------------------------------------------
1 | declare var ENV_PRODUCTION: boolean
2 |
3 | export const PRODUCTION = ENV_PRODUCTION
4 |
--------------------------------------------------------------------------------
/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uryyyyyyy/react-redux-sample/HEAD/public/images/favicon.ico
--------------------------------------------------------------------------------
/src/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default function NotFound() {
4 | return
Not Found
5 | }
6 |
--------------------------------------------------------------------------------
/src/__tests__/enzyme-util.ts:
--------------------------------------------------------------------------------
1 | import * as Enzyme from 'enzyme'
2 | import * as Adapter from 'enzyme-adapter-react-16'
3 | Enzyme.configure({ adapter: new Adapter() })
4 |
--------------------------------------------------------------------------------
/moveToPublic.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | rm -rf ./public/libs/
4 | mkdir -p ./public/libs/
5 | cp ./node_modules/react/umd/react.production.min.js ./public/libs/
6 | cp ./node_modules/react-dom/umd/react-dom.production.min.js ./public/libs/
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | tabWidth: 2,
4 | useTabs: false,
5 | semi: false,
6 | singleQuote: true,
7 | bracketSpacing: true,
8 | arrowParens: 'always',
9 | parser: 'typescript'
10 | }
11 |
--------------------------------------------------------------------------------
/src/home/Home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | const styles = require('./style.scss')
3 |
4 | export function Home() {
5 | return (
6 |
7 |
Home
8 |
{"it's home."}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/index.template.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Sample
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import counter, { CounterActions, CounterState } from './counter/module'
2 | import upload, { FileUploadActions, FileUploadState } from './upload/module'
3 | import { createStore, combineReducers, Action } from 'redux'
4 |
5 | export default createStore(
6 | combineReducers({
7 | counter,
8 | upload
9 | })
10 | )
11 |
12 | export interface ReduxState {
13 | counter: CounterState
14 | upload: FileUploadState
15 | }
16 |
17 | export type ReduxAction = CounterActions | FileUploadActions | Action
18 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:8.9.4-browsers
6 | environment:
7 | TZ: "/usr/share/zoneinfo/Asia/Tokyo"
8 | steps:
9 | - checkout
10 | - restore_cache:
11 | key: dependency-cache
12 | - run: npm install --no-save
13 | - save_cache:
14 | key: dependency-cache
15 | paths:
16 | - "~/.npm/_cacache"
17 | - run: npm run lint:all
18 | - run: npm run build:prod
19 | - run: npm run test:all
20 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": [
3 | "plugin:prettier/recommended",
4 | "plugin:react/recommended"
5 | ],
6 | "plugins": [
7 | "@typescript-eslint",
8 | "prettier",
9 | "react"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "sourceType": "module",
14 | "project": "./tsconfig.json"
15 | },
16 | "rules": {
17 | },
18 | "settings": {
19 | "react": {
20 | "version": "detect"
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictBindCallApply": true,
4 | "strictNullChecks": true,
5 | "noUnusedLocals" : true,
6 | "noImplicitThis": true,
7 | "noUnusedParameters": true,
8 | "alwaysStrict": true,
9 | "noImplicitAny": true,
10 | "strictFunctionTypes": true,
11 | "strictPropertyInitialization": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "moduleResolution": "node",
15 | "outDir": "./dist/",
16 | "sourceMap": true,
17 | "lib": ["dom", "es2017"],
18 | "module": "ES2015",
19 | "target": "es5",
20 | "jsx": "react"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/counter/__tests__/module.spec.ts:
--------------------------------------------------------------------------------
1 | import reducer, { decrementAmount, CounterState, incrementAmount } from '../module'
2 | import { PRODUCTION } from '../../Variables'
3 |
4 | describe('counter/module', () => {
5 | it('INCREMENT', () => {
6 | const state: CounterState = { num: 4, loadingCount: 0 }
7 | const result = reducer(state, incrementAmount(3))
8 | expect(result.num).toBe(state.num + 3)
9 | })
10 |
11 | it('DECREMENT', () => {
12 | const state: CounterState = { num: 4, loadingCount: 0 }
13 | const result = reducer(state, decrementAmount(3))
14 | expect(result.num).toBe(state.num - 3)
15 | })
16 |
17 | it('check Env value', () => {
18 | expect(PRODUCTION).toBe(false)
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/src/Index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import { Router } from 'react-router'
4 | import store from './store'
5 | import { Provider } from 'react-redux'
6 | import { createBrowserHistory } from 'history'
7 | import { Routes } from './Routes'
8 | import { PRODUCTION } from './Variables'
9 | import HTML5Backend from 'react-dnd-html5-backend'
10 | import { DndProvider } from 'react-dnd'
11 |
12 | const history = createBrowserHistory()
13 |
14 | console.log(`this source is for ${PRODUCTION ? 'prod' : 'dev'}`)
15 |
16 | ReactDOM.render(
17 |
18 |
19 |
20 |
21 |
22 |
23 | ,
24 | document.getElementById('app')
25 | )
26 |
--------------------------------------------------------------------------------
/src/counter/Counter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { CounterState } from './module'
3 | import { ActionDispatcher } from './Container'
4 | const styles = require('./style.scss')
5 |
6 | interface Props {
7 | value: CounterState
8 | actions: ActionDispatcher
9 | param?: string
10 | }
11 |
12 | export function Counter(props: Props) {
13 | return (
14 |
15 | {props.param === undefined ? null :
{props.param}
}
16 |
{`score: ${props.value.num}`}
17 |
18 |
19 |
20 | {props.value.loadingCount === 0 ? null :
loading
}
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/upload/FileUploadSample.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { FileUploadState } from './module'
3 | import { ActionDispatcher } from './Container'
4 |
5 | interface Props {
6 | value: FileUploadState
7 | actions: ActionDispatcher
8 | }
9 |
10 | export function FileUploadSample(props: Props) {
11 | const handleChangeFile = (e: any) => {
12 | const target: HTMLInputElement = e.target as HTMLInputElement
13 | if (target.files == null) {
14 | props.actions.updateFile(null)
15 | } else {
16 | const _file = target.files.item(0)
17 | props.actions.updateFile(_file)
18 | }
19 | }
20 | const file = props.value.file
21 | const button = file ? : null
22 | return (
23 |
24 |
File upload
25 | handleChangeFile(e)} />
26 | {button}
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 |
2 | ## React-redux-sample
3 |
4 | [](https://circleci.com/gh/uryyyyyyy/react-redux-sample/tree/more-tools)
5 |
6 | minimal sample implementation
7 |
8 | see document about it(Japanese)
9 |
10 | - [helloWorld](http://qiita.com/uryyyyyyy/items/63969d6ed9341affdffb)
11 | - [redux](http://qiita.com/uryyyyyyy/items/3ad88cf9ca9393335f8c)
12 | - [redux-test](http://qiita.com/uryyyyyyy/items/7d4b0ede3f2b973d6951)
13 | - [async](http://qiita.com/uryyyyyyy/items/41334a810f1501ece87d)
14 | - [react-router](http://qiita.com/uryyyyyyy/items/30733e9cd140e60c52bd)
15 | - lint & formatter (coming soon)
16 | - build-tuning (coming soon)
17 |
18 | ## requirement
19 |
20 | - NodeJS 8.9~
21 | - NPM 5.3~
22 | - Chrome Browser (or fetch-API implemented browser)
23 |
24 | ## setup
25 |
26 | `npm install`
27 |
28 | ## build
29 |
30 | `npm run build`
31 |
32 | ## run
33 |
34 | `npm run server`
35 |
36 | ## License
37 |
38 | MIT
39 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2017 uryyyyyyy
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/src/counter/__tests__/Counter.spec.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Counter } from '../Counter'
3 | import '../../__tests__/enzyme-util'
4 | import { shallow } from 'enzyme'
5 | import { CounterState } from '../module'
6 | import { ActionDispatcher } from '../Container'
7 |
8 | describe('Counter', () => {
9 | it('rendering', () => {
10 | const actions: any = {}
11 | const state: CounterState = { num: 1, loadingCount: 1 }
12 | const wrapper = shallow()
13 | expect(
14 | wrapper
15 | .find('p')
16 | .at(0)
17 | .prop('children')
18 | ).toBe('score: 1')
19 | expect(
20 | wrapper
21 | .find('p')
22 | .at(1)
23 | .prop('children')
24 | ).toBe('loading')
25 | })
26 |
27 | it('click', () => {
28 | const actionSpy = new ActionDispatcher(null!)
29 | spyOn(actionSpy, 'increment')
30 | const state: CounterState = { num: 0, loadingCount: 0 }
31 | const wrapper = shallow()
32 | wrapper
33 | .find('button')
34 | .at(0)
35 | .simulate('click')
36 | expect(actionSpy.increment).toHaveBeenCalledWith(3)
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/config/dev-server.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const express = require('express')
4 | const app = express()
5 |
6 | const multer = require('multer')
7 | const upload = multer({ dest: './uploads/' })
8 |
9 | app.use('/public', express.static('public'))
10 |
11 | app.get('/api/count', (req, res) => {
12 | res.contentType('application/json')
13 | const obj = { amount: 100 }
14 | setTimeout(() => res.json(obj), 500)
15 | //res.status(400).json(obj); //for error testing
16 | })
17 |
18 | app.post('/api/upload', upload.fields([{ name: 'myFile' }]), (req, res) => {
19 | const myFile = req.files.myFile[0]
20 | const tmp_path = myFile.path
21 | const target_path = './uploads/' + myFile.originalname
22 | fs.rename(tmp_path, target_path, (err) => {
23 | if (err) throw err
24 | fs.unlink(tmp_path, function() {
25 | if (err) throw err
26 | setTimeout(() => res.json({ message: `File uploaded to: ${target_path} - ${myFile.size} bytes` }), 500)
27 | })
28 | })
29 | })
30 |
31 | app.get('*', (req, res) => {
32 | res.sendFile(path.join(__dirname, '../public/index.html'))
33 | })
34 |
35 | app.listen(3000, (err) => {
36 | if (err) {
37 | console.log(err)
38 | }
39 | console.log('server start at port 3000')
40 | })
41 |
--------------------------------------------------------------------------------
/src/Routes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Switch } from 'react-router'
3 | import { Link, Route } from 'react-router-dom'
4 | import Counter from './counter/Container'
5 | import NotFound from './NotFound'
6 | import { Home } from './home/Home'
7 | import ReactDNDSample from './dnd/Container'
8 | import FileUploadSample from './upload/Container'
9 |
10 | export function Routes() {
11 | return (
12 |
13 |
React Redux sample
14 |
15 | Home
16 |
17 |
18 | Counter
19 |
20 |
21 | Counter with param
22 |
23 |
24 | Drag and Drop
25 |
26 |
27 | file upload
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/config/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const HtmlWebpackPlugin = require('html-webpack-plugin')
4 | const webpack = require('webpack')
5 | const env = require('./variables/prod')
6 |
7 | module.exports = {
8 | mode: 'production',
9 | entry: './src/Index.tsx',
10 | output: {
11 | filename: 'bundle/main.[chunkhash].js',
12 | path: path.resolve('public'),
13 | publicPath: '/public'
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.tsx?$/,
19 | loader: 'ts-loader'
20 | },
21 | {
22 | test: /\.scss$/,
23 | use: [
24 | {
25 | loader: "style-loader"
26 | }, {
27 | loader: "css-loader",
28 | options: {
29 | importLoaders: 1,
30 | modules: true
31 | }
32 | }, {
33 | loader: "sass-loader"
34 | }
35 | ]
36 | }
37 | ]
38 | },
39 | resolve: {
40 | extensions: ['.ts', '.tsx', '.js']
41 | },
42 | externals: {
43 | react: 'React',
44 | 'react-dom': 'ReactDOM'
45 | },
46 | plugins: [
47 | new HtmlWebpackPlugin({
48 | filename: 'index.html',
49 | template: 'index.template.ejs',
50 | inject: 'body'
51 | }),
52 | new webpack.DefinePlugin(env)
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const HtmlWebpackPlugin = require('html-webpack-plugin')
4 | const webpack = require('webpack')
5 | const env = require('./variables/dev')
6 |
7 | module.exports = {
8 | mode: 'development',
9 | devtool: 'source-map',
10 | entry: './src/Index.tsx',
11 | output: {
12 | filename: 'bundle/main.[chunkhash].js',
13 | path: path.resolve('public'),
14 | publicPath: '/public'
15 | },
16 | module: {
17 | rules: [
18 | {
19 | test: /\.tsx?$/,
20 | loader: 'ts-loader'
21 | },
22 | {
23 | test: /\.scss$/,
24 | use: [
25 | {
26 | loader: 'style-loader'
27 | },
28 | {
29 | loader: 'css-loader',
30 | options: {
31 | importLoaders: 1,
32 | modules: true
33 | }
34 | },
35 | {
36 | loader: 'sass-loader'
37 | }
38 | ]
39 | }
40 | ]
41 | },
42 | resolve: {
43 | extensions: ['.ts', '.tsx', '.js']
44 | },
45 | externals: {
46 | react: 'React',
47 | 'react-dom': 'ReactDOM'
48 | },
49 | plugins: [
50 | new HtmlWebpackPlugin({
51 | filename: 'index.html',
52 | template: 'index.template.ejs',
53 | inject: 'body'
54 | }),
55 | new webpack.DefinePlugin(env)
56 | ]
57 | }
58 |
--------------------------------------------------------------------------------
/src/dnd/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { useCallback, useState } from 'react'
3 | import Card from './Card'
4 | import * as _ from 'lodash'
5 | import { RouteComponentProps } from 'react-router'
6 |
7 | interface ICard {
8 | id: number
9 | text: string
10 | }
11 |
12 | const initialCards: ICard[] = [
13 | {
14 | id: 1,
15 | text: 'Write a cool JS library'
16 | },
17 | {
18 | id: 2,
19 | text: 'Make it generic enough'
20 | },
21 | {
22 | id: 3,
23 | text: 'Write README'
24 | },
25 | {
26 | id: 4,
27 | text: 'Create some examples'
28 | },
29 | {
30 | id: 5,
31 | text: 'Spam in Twitter and IRC to promote it (note that this element is taller than the others)'
32 | },
33 | {
34 | id: 6,
35 | text: '???'
36 | },
37 | {
38 | id: 7,
39 | text: 'PROFIT'
40 | }
41 | ]
42 |
43 | export default function Container(_unused: RouteComponentProps<{}>) {
44 | const [cards, setCards] = useState(initialCards)
45 |
46 | const moveCard = useCallback(
47 | (dragIndex: number, hoverIndex: number) => {
48 | const targetList = _.clone(cards)
49 | const target = _.nth(targetList, dragIndex)
50 | if (!target) return
51 | targetList.splice(dragIndex, 1)
52 | targetList.splice(hoverIndex, 0, target)
53 | setCards(targetList)
54 | },
55 | [cards]
56 | )
57 |
58 | return (
59 |
60 | {cards.map((card: ICard, i: number) => (
61 |
62 | ))}
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/src/upload/module.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux'
2 | import { ReduxAction } from '../store'
3 |
4 | enum ActionNames {
5 | UPDATE_FILE = 'upload/update_file',
6 | UPLOAD_START = 'upload/fetch_request_start',
7 | UPLOAD_FINISH = 'upload/fetch_request_finish'
8 | }
9 |
10 | interface UpdateFileAction extends Action {
11 | type: ActionNames.UPDATE_FILE
12 | appAction: boolean
13 | file: File | null
14 | }
15 | export const updateFile = (file: File | null): UpdateFileAction => ({
16 | type: ActionNames.UPDATE_FILE,
17 | appAction: true,
18 | file
19 | })
20 |
21 | interface UploadStartAction extends Action {
22 | type: ActionNames.UPLOAD_START
23 | appAction: boolean
24 | }
25 | export const uploadStart = (): UploadStartAction => ({
26 | type: ActionNames.UPLOAD_START,
27 | appAction: true
28 | })
29 |
30 | interface UploadFinishAction extends Action {
31 | type: ActionNames.UPLOAD_FINISH
32 | appAction: boolean
33 | }
34 | export const uploadFinish = (): UploadFinishAction => ({
35 | type: ActionNames.UPLOAD_FINISH,
36 | appAction: true
37 | })
38 |
39 | export interface FileUploadState {
40 | file: File | null
41 | }
42 |
43 | export type FileUploadActions = UploadStartAction | UploadFinishAction | UpdateFileAction
44 |
45 | const initialState: FileUploadState = {
46 | file: null
47 | }
48 |
49 | export default function reducer(state: FileUploadState = initialState, action: ReduxAction): FileUploadState {
50 | if (!('appAction' in action)) {
51 | return state
52 | }
53 | switch (action.type) {
54 | case ActionNames.UPDATE_FILE: {
55 | return Object.assign({}, state, { file: action.file })
56 | }
57 | default:
58 | return state
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const env = require('./config/variables/dev')
3 |
4 | const args = process.argv
5 | args.splice(0, 4)
6 |
7 | const polyfills = []
8 |
9 | const files = polyfills.concat(args)
10 |
11 | module.exports = (config) => {
12 | config.set({
13 | basePath: '',
14 | frameworks: ['jasmine'],
15 | files: files,
16 | preprocessors: {
17 | '**/*.spec.ts': ['webpack'],
18 | '**/*.spec.tsx': ['webpack']
19 | },
20 | mime: {
21 | 'text/x-typescript': ['ts', 'tsx']
22 | },
23 | webpack: {
24 | resolve: {
25 | extensions: ['.ts', '.js', '.tsx']
26 | },
27 | module: {
28 | rules: [
29 | {
30 | test: /\.tsx?$/,
31 | use: [
32 | {loader: "ts-loader"}
33 | ]
34 | },
35 | {
36 | test: /\.scss$/,
37 | use: [
38 | {
39 | loader: "style-loader"
40 | }, {
41 | loader: "css-loader",
42 | options: {
43 | importLoaders: 1,
44 | modules: true
45 | }
46 | }, {
47 | loader: "sass-loader"
48 | }
49 | ]
50 | }
51 | ]
52 | },
53 | plugins: [new webpack.DefinePlugin(env)]
54 | },
55 | webpackMiddleware: {
56 | stats: 'errors-only',
57 | noInfo: true
58 | },
59 | reporters: ['mocha'],
60 | port: 9876,
61 | colors: true,
62 | logLevel: config.LOG_INFO,
63 | autoWatch: false,
64 | browsers: ['ChromeHeadless'],
65 | singleRun: true,
66 | concurrency: Infinity
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/src/counter/__tests__/ActionDispatcher.spec.ts:
--------------------------------------------------------------------------------
1 | import * as fetchMock from 'fetch-mock'
2 | import { incrementAmount, fetchRequestStart, fetchRequestFinish } from '../module'
3 | import { ActionDispatcher } from '../Container'
4 |
5 | describe('ActionDispatcher', () => {
6 | beforeEach(() => {
7 | fetchMock.restore()
8 | })
9 |
10 | it('increment', () => {
11 | const spy: any = { dispatch: null }
12 | spyOn(spy, 'dispatch')
13 | const actions = new ActionDispatcher(spy.dispatch)
14 | actions.increment(100)
15 | expect(spy.dispatch.calls.count()).toEqual(1)
16 | expect(spy.dispatch.calls.argsFor(0)[0]).toEqual(incrementAmount(100))
17 | })
18 |
19 | it('fetchAmount success', async (done) => {
20 | fetchMock.get('/api/count', { body: { amount: 100 }, status: 200 })
21 |
22 | const spy: any = { dispatch: null }
23 | spyOn(spy, 'dispatch')
24 | const actions = new ActionDispatcher(spy.dispatch)
25 | await actions.asyncIncrement()
26 | expect(spy.dispatch.calls.count()).toEqual(3)
27 | expect(spy.dispatch.calls.argsFor(0)[0]).toEqual(fetchRequestStart())
28 | expect(spy.dispatch.calls.argsFor(1)[0]).toEqual(incrementAmount(100))
29 | expect(spy.dispatch.calls.argsFor(2)[0]).toEqual(fetchRequestFinish())
30 | done()
31 | })
32 |
33 | it('fetchAmount fail', async (done) => {
34 | fetchMock.get('/api/count', { body: {}, status: 400 })
35 |
36 | const spy: any = { dispatch: null }
37 | spyOn(spy, 'dispatch')
38 | const actions = new ActionDispatcher(spy.dispatch)
39 | await actions.asyncIncrement()
40 | expect(spy.dispatch.calls.count()).toEqual(2)
41 | expect(spy.dispatch.calls.argsFor(0)[0]).toEqual(fetchRequestStart())
42 | expect(spy.dispatch.calls.argsFor(1)[0]).toEqual(fetchRequestFinish())
43 | done()
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/counter/Container.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Counter } from './Counter'
3 | import { useDispatch, useSelector } from 'react-redux'
4 | import {
5 | CounterActions,
6 | CounterState,
7 | decrementAmount,
8 | fetchRequestFinish,
9 | fetchRequestStart,
10 | incrementAmount
11 | } from './module'
12 | import { Dispatch } from 'redux'
13 | import { ReduxState } from '../store'
14 | import { RouteComponentProps } from 'react-router'
15 |
16 | export class ActionDispatcher {
17 | constructor(private dispatch: Dispatch) {}
18 |
19 | myHeaders = new Headers({
20 | 'Content-Type': 'application/json',
21 | Accept: 'application/json',
22 | 'X-Requested-With': 'XMLHttpRequest'
23 | })
24 |
25 | public increment(amount: number): void {
26 | this.dispatch(incrementAmount(amount))
27 | }
28 |
29 | public decrement(amount: number): void {
30 | this.dispatch(decrementAmount(amount))
31 | }
32 |
33 | public async asyncIncrement(): Promise {
34 | this.dispatch(fetchRequestStart())
35 |
36 | try {
37 | const response: Response = await fetch('/api/count', {
38 | method: 'GET',
39 | headers: this.myHeaders
40 | })
41 |
42 | if (response.status !== 200) {
43 | throw new Error(`illegal status code: ${response.status}`)
44 | }
45 | const json: { amount: number } = await response.json()
46 | this.dispatch(incrementAmount(json.amount))
47 | } catch (err) {
48 | console.error(err)
49 | } finally {
50 | this.dispatch(fetchRequestFinish())
51 | }
52 | }
53 | }
54 |
55 | type RouterProps = RouteComponentProps<{ myParams: string | undefined }>
56 |
57 | export default function CounterContainer(props: RouterProps) {
58 | const count = useSelector((state) => {
59 | return state.counter
60 | })
61 | return
62 | }
63 |
--------------------------------------------------------------------------------
/src/upload/Container.tsx:
--------------------------------------------------------------------------------
1 | import { FileUploadSample } from './FileUploadSample'
2 | import { connect, MapDispatchToPropsParam, MapStateToPropsParam } from 'react-redux'
3 | import { Dispatch } from 'redux'
4 | import { FileUploadState, updateFile, uploadFinish, uploadStart } from './module'
5 | import { ReduxAction, ReduxState } from '../store'
6 | import { RouteComponentProps } from 'react-router'
7 |
8 | export class ActionDispatcher {
9 | constructor(private dispatch: (action: ReduxAction) => void) {}
10 |
11 | myHeaders = new Headers({
12 | Accept: 'application/json',
13 | 'X-Requested-With': 'XMLHttpRequest'
14 | })
15 |
16 | public updateFile(file: File | null): void {
17 | this.dispatch(updateFile(file))
18 | }
19 |
20 | public async upload(file: File): Promise {
21 | this.dispatch(uploadStart())
22 | const formData = new FormData()
23 | formData.append('myFile', file)
24 | try {
25 | const response: Response = await fetch('/api/upload', {
26 | method: 'POST',
27 | body: formData,
28 | headers: this.myHeaders
29 | })
30 | if (response.ok) {
31 | // 2xx
32 | const json = await response.json()
33 | console.log(json)
34 | alert('upload done')
35 | } else {
36 | throw new Error(`illegal status code: ${response.status}`)
37 | }
38 | } catch (err) {
39 | console.error(err)
40 | } finally {
41 | this.dispatch(uploadFinish())
42 | }
43 | }
44 | }
45 |
46 | type RouterProps = RouteComponentProps<{}>
47 |
48 | const mapStateToProps: MapStateToPropsParam<{ value: FileUploadState }, RouterProps, ReduxState> = (
49 | state: ReduxState,
50 | _: RouterProps
51 | ) => {
52 | return { value: state.upload }
53 | }
54 |
55 | const mapDispatchToProps: MapDispatchToPropsParam<{ actions: ActionDispatcher }, {}> = (
56 | dispatch: Dispatch
57 | ) => ({ actions: new ActionDispatcher(dispatch) })
58 |
59 | export default connect(
60 | mapStateToProps,
61 | mapDispatchToProps
62 | )(FileUploadSample)
63 |
--------------------------------------------------------------------------------
/src/counter/module.ts:
--------------------------------------------------------------------------------
1 | import { Action } from 'redux'
2 |
3 | enum ActionNames {
4 | INC = 'counter/increment',
5 | DEC = 'counter/decrement',
6 | FETCH_START = 'counter/fetch_request_start',
7 | FETCH_FINISH = 'counter/fetch_request_finish'
8 | }
9 |
10 | interface IncrementAction extends Action {
11 | type: ActionNames.INC
12 | plusAmount: number
13 | }
14 | export const incrementAmount = (amount: number): IncrementAction => ({
15 | type: ActionNames.INC,
16 | plusAmount: amount
17 | })
18 |
19 | interface DecrementAction extends Action {
20 | type: ActionNames.DEC
21 | minusAmount: number
22 | }
23 | export const decrementAmount = (amount: number): DecrementAction => ({
24 | type: ActionNames.DEC,
25 | minusAmount: amount
26 | })
27 |
28 | interface FetchRequestStartAction extends Action {
29 | type: ActionNames.FETCH_START
30 | }
31 | export const fetchRequestStart = (): FetchRequestStartAction => ({
32 | type: ActionNames.FETCH_START
33 | })
34 |
35 | interface FetchRequestFinishAction extends Action {
36 | type: ActionNames.FETCH_FINISH
37 | }
38 | export const fetchRequestFinish = (): FetchRequestFinishAction => ({
39 | type: ActionNames.FETCH_FINISH
40 | })
41 |
42 | export interface CounterState {
43 | num: number
44 | loadingCount: number
45 | }
46 |
47 | export type CounterActions = IncrementAction | DecrementAction | FetchRequestStartAction | FetchRequestFinishAction
48 |
49 | const initialState: CounterState = {
50 | num: 0,
51 | loadingCount: 0
52 | }
53 |
54 | export default function reducer(state: CounterState = initialState, action: CounterActions): CounterState {
55 | switch (action.type) {
56 | case ActionNames.INC:
57 | return Object.assign({}, state, { num: state.num + action.plusAmount })
58 | case ActionNames.DEC:
59 | return Object.assign({}, state, { num: state.num - action.minusAmount })
60 | case ActionNames.FETCH_START: {
61 | return Object.assign({}, state, { loadingCount: state.loadingCount + 1 })
62 | }
63 | case ActionNames.FETCH_FINISH: {
64 | return Object.assign({}, state, { loadingCount: state.loadingCount - 1 })
65 | }
66 | default:
67 | return state
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-typescript-redux",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "postinstall": "sh ./moveToPublic.sh",
7 | "build": "webpack --config ./config/webpack.config.dev.js",
8 | "watch": "webpack --config ./config/webpack.config.dev.js --watch",
9 | "build:prod": "webpack --config ./config/webpack.config.prod.js",
10 | "test:unit": "karma start karma.conf.js",
11 | "test:all": "karma start karma.conf.js **/*.spec.tsx **/*.spec.ts",
12 | "server": "node ./config/dev-server.js",
13 | "lint:unit": "eslint",
14 | "lint:all": "eslint 'src/**/*.ts?(x)'",
15 | "format:all": "prettier --config ./prettier.config.js --write 'src/**/*.ts?(x)'",
16 | "format:unit": "prettier --config ./prettier.config.js --write"
17 | },
18 | "devDependencies": {
19 | "@types/enzyme": "3.10.3",
20 | "@types/enzyme-adapter-react-16": "1.0.5",
21 | "@types/fetch-mock": "7.3.1",
22 | "@types/jasmine": "3.4.0",
23 | "@types/lodash": "4.14.136",
24 | "@types/react": "16.9.1",
25 | "@types/react-dom": "16.8.5",
26 | "@types/react-redux": "7.1.1",
27 | "@types/react-router-dom": "4.3.4",
28 | "@typescript-eslint/eslint-plugin": "1.13.0",
29 | "@typescript-eslint/parser": "1.13.0",
30 | "css-loader": "3.2.0",
31 | "enzyme": "3.10.0",
32 | "enzyme-adapter-react-16": "1.14.0",
33 | "eslint": "6.1.0",
34 | "eslint-config-prettier": "6.0.0",
35 | "eslint-plugin-prettier": "3.1.0",
36 | "eslint-plugin-react": "7.14.3",
37 | "express": "4.17.1",
38 | "fetch-mock": "7.3.9",
39 | "html-webpack-plugin": "3.2.0",
40 | "jasmine-core": "3.4.0",
41 | "karma": "4.2.0",
42 | "karma-chrome-launcher": "3.0.0",
43 | "karma-jasmine": "2.0.1",
44 | "karma-mocha-reporter": "2.2.5",
45 | "karma-webpack": "4.0.2",
46 | "multer": "1.4.2",
47 | "node-sass": "4.12.0",
48 | "prettier": "1.18.2",
49 | "react-test-renderer": "16.9.0",
50 | "sass-loader": "7.2.0",
51 | "style-loader": "1.0.0",
52 | "ts-loader": "6.0.4",
53 | "typescript": "3.5.3",
54 | "webpack": "4.39.1",
55 | "webpack-cli": "3.3.6"
56 | },
57 | "dependencies": {
58 | "lodash": "4.17.15",
59 | "react": "16.9.0",
60 | "react-dnd": "9.3.4",
61 | "react-dnd-html5-backend": "9.3.4",
62 | "react-dom": "16.9.0",
63 | "react-redux": "7.1.0",
64 | "react-router-dom": "5.0.1",
65 | "redux": "4.0.4"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/dnd/Card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { useRef } from 'react'
3 | import { DragSourceMonitor, DropTargetMonitor, useDrag, useDrop } from 'react-dnd'
4 | import { XYCoord } from 'dnd-core'
5 |
6 | const ItemTypes = {
7 | CARD: 'card'
8 | }
9 |
10 | interface Props {
11 | index: number
12 | id: number
13 | text: string
14 | moveCard: (dragIndex: number, hoverIndex: number) => void
15 | }
16 |
17 | export default function CardA(props: Props) {
18 | const ref = useRef(null)
19 | const [{ isDragging }, drag] = useDrag({
20 | item: {
21 | id: props.id,
22 | index: props.index,
23 | type: ItemTypes.CARD
24 | },
25 | collect: (monitor: DragSourceMonitor) => ({
26 | isDragging: monitor.isDragging()
27 | })
28 | })
29 |
30 | const [, drop] = useDrop({
31 | accept: ItemTypes.CARD,
32 | hover: (item: any, monitor: DropTargetMonitor) => {
33 | if (!ref.current) {
34 | return
35 | }
36 | const dragIndex = item.index
37 | const hoverIndex = props.index
38 |
39 | // Don't replace items with themselves
40 | if (dragIndex === hoverIndex) {
41 | return
42 | }
43 |
44 | // Determine rectangle on screen
45 | const hoverBoundingRect = ref.current!.getBoundingClientRect()
46 |
47 | // Get vertical middle
48 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
49 |
50 | // Determine mouse position
51 | const clientOffset = monitor.getClientOffset()
52 |
53 | // Get pixels to the top
54 | const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top
55 |
56 | // Only perform the move when the mouse has crossed half of the items height
57 | // When dragging downwards, only move when the cursor is below 50%
58 | // When dragging upwards, only move when the cursor is above 50%
59 |
60 | // Dragging downwards
61 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
62 | return
63 | }
64 |
65 | // Dragging upwards
66 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
67 | return
68 | }
69 |
70 | // Time to actually perform the action
71 | props.moveCard(dragIndex, hoverIndex)
72 |
73 | // Note: we're mutating the monitor item here!
74 | // Generally it's better to avoid mutations,
75 | // but it's good here for the sake of performance
76 | // to avoid expensive index searches.
77 | item.index = hoverIndex
78 | }
79 | })
80 | const opacity = isDragging ? 0 : 1
81 |
82 | drag(drop(ref))
83 | return (
84 |
95 | {props.text}
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------