diff --git a/00 Base/config/webpack/base.js b/00 Base/config/webpack/base.js index dff4c7b..d89604e 100644 --- a/00 Base/config/webpack/base.js +++ b/00 Base/config/webpack/base.js @@ -8,10 +8,7 @@ module.exports = merge( { context: helpers.resolveFromRootPath('src'), resolve: { - extensions: ['.js', '.ts', '.tsx'], - modules: [ - 'node_modules' - ] + extensions: ['.js', '.ts', '.tsx', '.css', '.scss', '.less'], }, entry: { app: ['./index.tsx'], @@ -28,14 +25,18 @@ module.exports = merge( babelCore: '@babel/core', } }, + { + test: /\.css$/, + loaders: ['style-loader', 'css-loader'], + }, + { + test: /\.s[ac]ss$/i, + loaders: ['style-loader', 'css-loader', 'sass-loader'], + }, { test: /\.less$/, - use: [ - { loader: 'style-loader' }, - { loader: 'css-loader' }, - { loader: 'less-loader' } - ] - } + use: ['style-loader', 'css-loader', 'less-loader'], + }, ], }, optimization: { diff --git a/00 Base/package.json b/00 Base/package.json index 2e1ce01..ed52777 100644 --- a/00 Base/package.json +++ b/00 Base/package.json @@ -13,10 +13,12 @@ "author": "arp82", "license": "MIT", "dependencies": { + "@material-ui/core": "^4.9.9", + "@material-ui/icons": "^4.9.1", + "axios": "^0.19.2", + "babel-polyfill": "^6.26.0", "@babel/preset-react": "^7.9.4", "@babel/preset-stage-0": "^7.8.3", - "@material-ui/core": "^4.9.9", - "axios": "^0.19.0", "babel-core": "^7.0.0-bridge.0", "identity-obj-proxy": "^3.0.0", "jest-transform-css": "^2.0.0", @@ -24,9 +26,10 @@ "react-dom": "^16.8.6", "react-redux": "^7.2.0", "react-router-dom": "^5.0.1", - "react-test-renderer": "^16.13.1", "redux": "^4.0.5", "redux-thunk": "^2.3.0", + "sass-loader": "^8.0.2", + "react-test-renderer": "^16.13.1", "sinon": "^9.0.1", "style-loader": "^1.1.3", "ts-jest": "^25.3.1" @@ -48,11 +51,17 @@ "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "html-webpack-plugin": "^3.2.0", - "jest": "^24.9.0", + "jest": "^24.8.0", "less": "^3.11.1", "less-loader": "^5.0.0", + "node-sass": "^4.13.1", "react-addons-test-utils": "^15.6.2", + "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.4", + "rimraf": "^2.6.3", + "sass-loader": "^7.2.0", + "style-loader": "^1.1.3", + "ts-jest": "^24.0.2", "typescript": "^3.5.2", "webpack": "^4.32.2", "webpack-cli": "^3.3.2", diff --git a/00 Base/src/app.tsx b/00 Base/src/app.tsx index 93864a7..f234558 100644 --- a/00 Base/src/app.tsx +++ b/00 Base/src/app.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { MessagesSection } from './components'; +import { MyComponent } from './components/myComponent'; +import './styles.less'; -export const App: React.FunctionComponent = props => ( +export const App: React.FunctionComponent = (props) => (
- +
); diff --git a/00 Base/src/components/myComponent/index.ts b/00 Base/src/components/myComponent/index.ts new file mode 100644 index 0000000..84cbb30 --- /dev/null +++ b/00 Base/src/components/myComponent/index.ts @@ -0,0 +1 @@ +export { MyComponent, Posts } from './myComponent'; diff --git a/00 Base/src/components/myComponent/myComponent.spec.tsx b/00 Base/src/components/myComponent/myComponent.spec.tsx new file mode 100644 index 0000000..decd453 --- /dev/null +++ b/00 Base/src/components/myComponent/myComponent.spec.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +// import { render, cleanup } from '@testing-library/react'; +import { + render, + fireEvent, + waitForElement, + act, + RenderResult, +} from '@testing-library/react'; +//import * as myApi from '../myApi'; +import { MyComponent, Props } from './myComponent'; +import { Provider } from 'react-redux'; +import * as redux from 'react-redux'; +import { createStore } from 'redux'; +import Enzyme, { mount } from 'enzyme'; +import EnzymeAdapter from 'enzyme-adapter-react-16'; +import { fetchPosts } from '../../redux/actions/index'; + +import reducer from '../../redux/reducers/index'; +jest.mock('./MyComponent.tsx', () => ({ + MyComponent: () =>
, +})); + +const baseProps: Props = { + nameFromProps: null, +}; +Enzyme.configure({ adapter: new EnzymeAdapter() }); + +//afterEach(cleanup); +describe('My component', () => { + let props: Props; + beforeEach(() => { + props = { ...baseProps }; + }); + + const name = 'Title'; + const getWrapper = ( + mockStore = createStore(reducer, { posts: { posts: [] } }) + ) => + mount( + + + + ); + + it('should display the title provided', () => { + const wrapper = getWrapper(); + expect(wrapper.find('h1').text()).toEqual(`Hello ${name}!`); + }); + + it('should dispatch the get post action', () => { + const mockStore = createStore(reducer, { posts: { posts: [] } }); + mockStore.dispatch = jest.fn(); + + const wrapper = getWrapper(mockStore); + wrapper.find('button').simulate('click'); + expect(mockStore.dispatch).toHaveBeenCalledTimes(1); + }); +}); diff --git a/00 Base/src/components/myComponent/myComponent.tsx b/00 Base/src/components/myComponent/myComponent.tsx new file mode 100644 index 0000000..c8e3ab6 --- /dev/null +++ b/00 Base/src/components/myComponent/myComponent.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; + +import { useSelector, useDispatch } from 'react-redux'; +import { fetchPosts, deletePost, addPost } from './../../redux/actions/index'; +import './styles.less'; + +import Button from '@material-ui/core/Button'; +import Chip from '@material-ui/core/Chip'; + +import { MyInputComponent } from '../myInputComponent/myInputComponent'; +export interface Props { + nameFromProps: string; +} +export interface Posts { + id: Number; + userId: Number; + title: string; + body: string; +} + +export const MyComponent: React.FunctionComponent = (props) => { + const { nameFromProps } = props; + const dispatch = useDispatch(); + const posts = useSelector((state) => state.posts.posts); + + return ( + <> +
+

Hello {nameFromProps}!

+ +
+ {!!posts + ? posts.map((el: Posts) => ( + dispatch(deletePost(el.id))} + /> + )) + : null} +
+ + dispatch( + addPost({ + id: Math.floor(Math.random() * 1000), + userId: 1, + title: newPost, + body: newPost, + }) + ) + } + /> +
+ + ); +}; diff --git a/00 Base/src/components/myComponent/styles.less b/00 Base/src/components/myComponent/styles.less new file mode 100644 index 0000000..3ec5f71 --- /dev/null +++ b/00 Base/src/components/myComponent/styles.less @@ -0,0 +1,12 @@ +.container { + text-align: center; + .chip-container { + display: block; + position: relative; + margin-top: 25px; + .MuiChip-root { + margin-left: 5px; + margin-top: 5px; + } + } +} diff --git a/00 Base/src/components/myInputComponent/index.ts b/00 Base/src/components/myInputComponent/index.ts new file mode 100644 index 0000000..3b0b11d --- /dev/null +++ b/00 Base/src/components/myInputComponent/index.ts @@ -0,0 +1 @@ +export { MyInputComponent } from './myInputComponent'; \ No newline at end of file diff --git a/00 Base/src/components/myInputComponent/myInputComponent.tsx b/00 Base/src/components/myInputComponent/myInputComponent.tsx new file mode 100644 index 0000000..bd63ca7 --- /dev/null +++ b/00 Base/src/components/myInputComponent/myInputComponent.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import TextField from '@material-ui/core/TextField'; +import { Add } from '@material-ui/icons'; +import './styles.less'; + +export interface Props { + addPost: Function; +} + +export const MyInputComponent: React.FunctionComponent = (props) => { + const { addPost } = props; + const [name, setName] = React.useState(''); + + return ( +
+ setName(e.target.value)} + /> + {!!name ? ( + { + addPost(name); + setName(''); + }} + /> + ) : null} +
+ ); +}; diff --git a/00 Base/src/components/myInputComponent/styles.less b/00 Base/src/components/myInputComponent/styles.less new file mode 100644 index 0000000..0af11b8 --- /dev/null +++ b/00 Base/src/components/myInputComponent/styles.less @@ -0,0 +1,9 @@ +.input-component { + .MuiFormControl-root { + margin-top: 10px; + } + .MuiSvgIcon-root { + margin-top: 35px; + color: #00ff00; + } +} diff --git a/00 Base/src/index.tsx b/00 Base/src/index.tsx index 1725370..2c35ff9 100644 --- a/00 Base/src/index.tsx +++ b/00 Base/src/index.tsx @@ -1,16 +1,14 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { App } from './app'; -import { Provider } from 'react-redux' -import { createStore, compose, applyMiddleware } from 'redux' -import {MessagesReducer} from './redux/reducers/MessagesReducer' -import thunk from 'redux-thunk' +import { Provider } from 'react-redux'; +import store from './redux/store'; -const MessagesStore = createStore(MessagesReducer,compose(applyMiddleware(thunk))); +import { App } from './app'; +import 'babel-polyfill' ReactDOM.render( - + -, - - document.getElementById('root')); + , + document.getElementById('root') +); diff --git a/00 Base/src/myApi/index.ts b/00 Base/src/myApi/index.ts new file mode 100644 index 0000000..3cbca37 --- /dev/null +++ b/00 Base/src/myApi/index.ts @@ -0,0 +1 @@ +export { getListOfFruit, getPosts } from './myApi'; diff --git a/00 Base/src/myApi/myApi.spec.tsx b/00 Base/src/myApi/myApi.spec.tsx new file mode 100644 index 0000000..49dbe83 --- /dev/null +++ b/00 Base/src/myApi/myApi.spec.tsx @@ -0,0 +1,19 @@ +import { getListOfFruit, getPosts } from './myApi'; + +describe('myApi tests', () => { + it('getListOfFruit should return an array of string', () => { + expect(getListOfFruit()).resolves.toEqual([ + 'grape', + 'pineapple', + 'watermelon', + 'orange', + 'lemon', + 'strawberry', + 'cherry', + 'peach', + ]); + }); + it('should fecth some post from the api', () => { + expect(getPosts()).rejects.toThrowError('Error fetching'); + }); +}); diff --git a/00 Base/src/myApi/myApi.ts b/00 Base/src/myApi/myApi.ts new file mode 100644 index 0000000..8ebc456 --- /dev/null +++ b/00 Base/src/myApi/myApi.ts @@ -0,0 +1,30 @@ +import * as BEApi from './myBackEndApiEndpoint'; +import axios from 'axios'; +import { Posts } from '../components/myComponent/index'; + +export const getListOfFruit = (): Promise => { + return BEApi.getFruits('http://fruityfruit.com') + .then(resolveFruits) + .catch(handleError); +}; + +const resolveFruits = (fruits: string[]) => { + return fruits; +}; + +const handleError = () => { + throw new Error('Where is my fruit???'); +}; +export const getPosts = (): Promise => { + return axios + .get('https://jsonplaceholder.typicode.com/posts') + .then(resolvePosts) + .catch(handlePostError); +}; +const resolvePosts = (posts: any) => { + console.log('resolvePosts -> posts', posts); + return posts.data; +}; +const handlePostError = () => { + throw new Error('Error fetching'); +}; diff --git a/00 Base/src/myApi/myBackEndApiEndpoint.ts b/00 Base/src/myApi/myBackEndApiEndpoint.ts new file mode 100644 index 0000000..1457042 --- /dev/null +++ b/00 Base/src/myApi/myBackEndApiEndpoint.ts @@ -0,0 +1,12 @@ +export const getFruits = (_url: string) => { + return Promise.resolve([ + 'grape', + 'pineapple', + 'watermelon', + 'orange', + 'lemon', + 'strawberry', + 'cherry', + 'peach', + ]); +} \ No newline at end of file diff --git a/00 Base/src/redux/actions/fruitsActions.tsx b/00 Base/src/redux/actions/fruitsActions.tsx new file mode 100644 index 0000000..81d68bf --- /dev/null +++ b/00 Base/src/redux/actions/fruitsActions.tsx @@ -0,0 +1,28 @@ +import { GET_ALL_FRUITS, DELETE_FRUIT, ADD_FRUIT } from './../constants'; +import { getListOfFruit } from '../../myApi/index'; + +export const getAllFruits = (fruits: String[]) => { + return { + type: GET_ALL_FRUITS, + payload: fruits, + }; +}; +export const fetchfruits = () => { + return (dispatch) => { + return getListOfFruit().then((res) => dispatch(getAllFruits(res))); + }; +}; + +export const deleteFruit = (name: string) => { + return { + type: DELETE_FRUIT, + payload: name, + }; +}; + +export const addFruit = (name: string) => { + return { + type: ADD_FRUIT, + payload: name, + }; +}; diff --git a/00 Base/src/redux/actions/index.ts b/00 Base/src/redux/actions/index.ts new file mode 100644 index 0000000..af7b50e --- /dev/null +++ b/00 Base/src/redux/actions/index.ts @@ -0,0 +1,7 @@ +export { + addFruit, + deleteFruit, + fetchfruits, + getAllFruits, +} from './fruitsActions'; +export { fetchPosts, deletePost,addPost } from './postActions'; diff --git a/00 Base/src/redux/actions/postActions.spec.tsx b/00 Base/src/redux/actions/postActions.spec.tsx new file mode 100644 index 0000000..8322e8d --- /dev/null +++ b/00 Base/src/redux/actions/postActions.spec.tsx @@ -0,0 +1,42 @@ +import { addPost, deletePost, fetchPosts } from './index'; +import { ADD_POST, DELETE_POST, GET_POST_FROM_API } from '../constants'; +import { getPostFromApi } from './postActions'; + +describe('actions', () => { + const testPost = { + id: 7, + userId: 1, + title: 'newPost', + body: 'newPost', + }; + const testPost2 = { + id: 8, + userId: 1, + title: 'newPost2', + body: 'newPost2', + }; + it('should create an action to add a post', () => { + const expectedAction = { + type: ADD_POST, + payload: testPost, + }; + expect(addPost(testPost)).toEqual(expectedAction); + }); + + it('should create an action to delete a post', () => { + const expectedAction = { + type: DELETE_POST, + payload: testPost.id, + }; + expect(deletePost(testPost.id)).toEqual(expectedAction); + }); + + it('should create an action to get all posts', () => { + const arrayOfPosts=[testPost,testPost2] + const expectedAction = { + type: GET_POST_FROM_API, + payload: arrayOfPosts, + }; + expect(getPostFromApi(arrayOfPosts)).toEqual(expectedAction); + }); +}); diff --git a/00 Base/src/redux/actions/postActions.tsx b/00 Base/src/redux/actions/postActions.tsx new file mode 100644 index 0000000..7831de9 --- /dev/null +++ b/00 Base/src/redux/actions/postActions.tsx @@ -0,0 +1,29 @@ +import { GET_POST_FROM_API, DELETE_POST, ADD_POST } from './../constants'; +import { getPosts } from '../../myApi/index'; +import { Posts } from '../../components/myComponent/index'; + +export const getPostFromApi = (posts: Posts[]) => { + return { + type: GET_POST_FROM_API, + payload: posts, + }; +}; +export const fetchPosts = () => { + return (dispatch) => { + return getPosts().then((res) => dispatch(getPostFromApi(res))); + }; +}; + +export const deletePost = (id: Number) => { + return { + type: DELETE_POST, + payload: id, + }; +}; + +export const addPost = (post: Posts) => { + return { + type: ADD_POST, + payload: post, + }; +}; diff --git a/00 Base/src/redux/constants.ts b/00 Base/src/redux/constants.ts new file mode 100644 index 0000000..1e7fd44 --- /dev/null +++ b/00 Base/src/redux/constants.ts @@ -0,0 +1,7 @@ +export const GET_ALL_FRUITS = 'GET_ALL_FRUITS'; +export const DELETE_FRUIT = 'DELETE_FRUIT'; +export const ADD_FRUIT = 'ADD_FRUIT'; + +export const GET_POST_FROM_API = 'GET_POST_FROM_API'; +export const DELETE_POST = 'DELETE_POST'; +export const ADD_POST = 'ADD_POST'; diff --git a/00 Base/src/redux/reducers/fruitReducer.tsx b/00 Base/src/redux/reducers/fruitReducer.tsx new file mode 100644 index 0000000..e260a37 --- /dev/null +++ b/00 Base/src/redux/reducers/fruitReducer.tsx @@ -0,0 +1,31 @@ +import { GET_ALL_FRUITS, DELETE_FRUIT, ADD_FRUIT } from './../constants'; +const initialState = { + fruitList: [], +}; +export default function (state = initialState, action) { + switch (action.type) { + case GET_ALL_FRUITS: { + return { + ...state, + fruitList: action.payload, + }; + } + case DELETE_FRUIT: { + return { + ...state, + fruitList: state.fruitList.filter((e: string) => e !== action.payload), + }; + } + case ADD_FRUIT: { + return { + ...state, + fruitList: state.fruitList.concat([action.payload]), + }; + } + default: { + return { + ...state, + }; + } + } +} diff --git a/00 Base/src/redux/reducers/index.ts b/00 Base/src/redux/reducers/index.ts new file mode 100644 index 0000000..2026315 --- /dev/null +++ b/00 Base/src/redux/reducers/index.ts @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import fruitsReducer from './fruitReducer'; +import postReducer from './postReducer' + +export default combineReducers({ + fruits: fruitsReducer, + posts: postReducer +}); diff --git a/00 Base/src/redux/reducers/postReducer.spec.tsx b/00 Base/src/redux/reducers/postReducer.spec.tsx new file mode 100644 index 0000000..2cfeed2 --- /dev/null +++ b/00 Base/src/redux/reducers/postReducer.spec.tsx @@ -0,0 +1,86 @@ +import reducer from './postReducer'; +import { + ADD_FRUIT, + DELETE_FRUIT, + GET_POST_FROM_API, + ADD_POST, + DELETE_POST, +} from '../constants'; + +const initialState = { + posts: [], +}; + +describe('post reducer', () => { + const testPost = { + userId: 1, + id: 1, + title: + 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: + 'quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto', + }; + const testPost2 = { + userId: 2, + id: 2, + title: 'another Title', + body: 'another Title', + }; + it('should return the initial state', () => { + expect(reducer(undefined, {})).toEqual({ + posts: [], + }); + }); + + it('should handle ADD_POST', () => { + expect( + reducer(initialState, { + type: ADD_POST, + payload: testPost, + }) + ).toEqual({ + posts: [testPost], + }); + + expect( + reducer( + { + posts: [testPost], + }, + { + type: ADD_POST, + payload: { + id: 7, + userId: 1, + title: 'newPost', + body: 'newPost', + }, + } + ) + ).toEqual({ + posts: [ + testPost, + { + id: 7, + userId: 1, + title: 'newPost', + body: 'newPost', + }, + ], + }); + }); + + it('should handle DELETE_POST', () => { + expect( + reducer( + { posts: [testPost, testPost2] }, + { + type: DELETE_POST, + payload: testPost2.id, + } + ) + ).toEqual({ + posts: [testPost], + }); + }); +}); diff --git a/00 Base/src/redux/reducers/postReducer.tsx b/00 Base/src/redux/reducers/postReducer.tsx new file mode 100644 index 0000000..e037c8f --- /dev/null +++ b/00 Base/src/redux/reducers/postReducer.tsx @@ -0,0 +1,32 @@ +import { GET_POST_FROM_API, DELETE_POST, ADD_POST } from './../constants'; +const initialState = { + posts: [], +}; +export default function (state = initialState, action) { + switch (action.type) { + case GET_POST_FROM_API: { + return { + ...state, + posts: action.payload, + }; + } + case DELETE_POST: { + return { + ...state, + posts: state.posts.filter((e) => e.id !== action.payload), + }; + } + + case ADD_POST: { + return { + ...state, + posts: state.posts.concat([action.payload]), + }; + } + default: { + return { + ...state, + }; + } + } +} diff --git a/00 Base/src/redux/store.js b/00 Base/src/redux/store.js new file mode 100644 index 0000000..e7c2c33 --- /dev/null +++ b/00 Base/src/redux/store.js @@ -0,0 +1,12 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './reducers/index'; + +const initialState = {}; +const middleware = [thunk]; +const store=createStore(rootReducer,initialState,compose( + applyMiddleware(thunk), + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() + +)) +export default store; diff --git a/00 Base/src/styles.less b/00 Base/src/styles.less new file mode 100644 index 0000000..af8d565 --- /dev/null +++ b/00 Base/src/styles.less @@ -0,0 +1,3 @@ +body { + background-color: #afafdc; +} diff --git a/00 Base/tsconfig.json b/00 Base/tsconfig.json index 8a26838..3e07872 100644 --- a/00 Base/tsconfig.json +++ b/00 Base/tsconfig.json @@ -11,7 +11,9 @@ "allowJs": true, "suppressImplicitAnyIndexErrors": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "outDir": "generated", }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "exclude": ["node_modules", "**/*.test.ts", "dist"] }