├── .babelrc ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── App ├── ActionCreators │ ├── ContentActionCreators.ts │ ├── HelloCountActionCreators.ts │ └── SubredditsActionCreators.ts ├── Actions │ ├── ContentLoadAction.ts │ ├── GetSubredditPostsActions.ts │ └── SayHelloAction.ts ├── App.tsx ├── Components │ ├── BaseComponent.tsx │ ├── Common │ │ └── Button │ │ │ ├── Button.module.less │ │ │ └── Button.tsx │ └── ContentPage │ │ ├── BodyWrapper │ │ ├── BodyWrapper.module.less │ │ ├── BodyWrapper.tsx │ │ └── resources │ │ │ └── headerlogo.png │ │ ├── ContentPage.module.less │ │ ├── ContentPage.tsx │ │ ├── Header │ │ ├── Header.module.less │ │ └── Header.tsx │ │ ├── Posts │ │ ├── Posts.module.less │ │ ├── Posts.tsx │ │ └── PostsList │ │ │ ├── PostsList.module.less │ │ │ └── PostsList.tsx │ │ └── SubredditChooser │ │ ├── SubredditChooser.module.less │ │ └── SubredditChooser.tsx ├── DataLayer │ └── SubredditsProvider.ts ├── Global │ └── Styles │ │ ├── global.less │ │ └── variables.less ├── Index.tsx ├── Models │ ├── Post.d.ts │ └── Subreddit.d..ts ├── Reducers │ ├── ContentReducer.ts │ ├── HelloCountReducer.ts │ ├── RootReducer.ts │ └── SubredditsReducer.ts └── Store │ ├── CreateStore.ts │ ├── State │ ├── ContentState.ts │ ├── HelloCountState.ts │ └── SubredditsState.ts │ └── StoreState.ts ├── LICENSE ├── README.md ├── babel └── babelhelpers.js ├── index.html ├── package.json ├── server.js ├── tsconfig.json ├── tsconfig.webpack.json ├── tslint.json ├── typings ├── global.d.ts └── react-redux.d.ts ├── webpack.config.js └── webpack.dev.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["external-helpers"], 3 | "presets": ["es2015-loose"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .awcache/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | script: 5 | - npm run build 6 | cache: 7 | bundler: true 8 | directories: 9 | - node_modules -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Controls if the editor will insert spaces for tabs. Accepted values: "auto", true, false. If set to "auto", the value will be guessed when a file is opened. 4 | "editor.insertSpaces": true, 5 | 6 | // Configure glob patterns for excluding files and folders. 7 | "files.exclude": { 8 | "**/.git": true, 9 | "**/.DS_Store": true, 10 | "node_modules/**": true, 11 | "build/**": true, 12 | ".awcache/**": true 13 | }, 14 | 15 | // When enabled, will trim trailing whitespace when you save a file. 16 | "files.trimTrailingWhitespace": true, 17 | 18 | // Specifies the folder path containing the tsserver and lib*.d.ts files to use. 19 | "typescript.tsdk": "node_modules/typescript/lib" 20 | } -------------------------------------------------------------------------------- /App/ActionCreators/ContentActionCreators.ts: -------------------------------------------------------------------------------- 1 | import { contentLoadAction, ContentLoadActionPayload } from "../Actions/ContentLoadAction"; 2 | 3 | export function loadContent(): ReduxActions.Action { 4 | const title: string = "Typescript-Webpack-React-Flux/Redux boilerplate"; 5 | const summary: string = "The goal of this example is to help you start when combining Typescript with React and Redux. " + 6 | "LESS in combination of CSS modules is used for styling components. For running, building and bundling there is powerful Webpack. " + 7 | "Check the console logs as you click around the page and install redux devtools chrome extenstion for better development experience."; 8 | 9 | return contentLoadAction(title, summary); 10 | } -------------------------------------------------------------------------------- /App/ActionCreators/HelloCountActionCreators.ts: -------------------------------------------------------------------------------- 1 | import { sayHelloAction } from "../Actions/SayHelloAction"; 2 | 3 | export function sayHello(): ReduxActions.Action { 4 | // Put your business logic here 5 | return sayHelloAction(); 6 | } -------------------------------------------------------------------------------- /App/ActionCreators/SubredditsActionCreators.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from "redux"; 2 | import { getSubredditPostsStartAction, getSubredditPostsSuccessAction, getSubredditPostsErrorAction } from "./../Actions/GetSubredditPostsActions"; 3 | import * as SubredditsProvider from "./../DataLayer/SubredditsProvider"; 4 | 5 | export function fetchPosts(subreddit: string): (dispatcher: Dispatch<{}>) => Promise<{}> { 6 | return (dispatch: Dispatch<{}>) => { 7 | dispatch(getSubredditPostsStartAction(subreddit)); 8 | 9 | return SubredditsProvider.fetchPosts(subreddit) 10 | .then((result: Post[]) => dispatch(getSubredditPostsSuccessAction(subreddit, result))) 11 | .catch(() => dispatch(getSubredditPostsErrorAction({subreddit, error: "FETCHING_ERROR"}))); 12 | }; 13 | } -------------------------------------------------------------------------------- /App/Actions/ContentLoadAction.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions"; 2 | 3 | export interface ContentLoadActionPayload { 4 | title: string; 5 | summary: string; 6 | } 7 | 8 | export const contentLoadAction = 9 | createAction("ContentLoadAction", (title, summary) => ({title, summary})); -------------------------------------------------------------------------------- /App/Actions/GetSubredditPostsActions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions"; 2 | 3 | export interface GetSubredditPostsStartActionPayload { 4 | subreddit: string; 5 | } 6 | 7 | export interface GetSubredditPostsSuccessActionPayload { 8 | subreddit: string; 9 | posts: Post[]; 10 | } 11 | 12 | export interface GetSubredditPostsErrorActionPayload { 13 | subreddit: string; 14 | error: string; 15 | } 16 | 17 | export const getSubredditPostsStartAction = 18 | createAction("GetSubredditPostsStartAction", (subreddit) => ({subreddit})); 19 | // an example utilizing payloadCreator 20 | export const getSubredditPostsSuccessAction = 21 | createAction("GetSubredditPostsSuccessAction", (subreddit, posts) => ({subreddit, posts})); 22 | // createAction without payloadCreator 23 | export const getSubredditPostsErrorAction = 24 | createAction("GetSubredditPostsErrorAction", undefined); -------------------------------------------------------------------------------- /App/Actions/SayHelloAction.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions"; 2 | 3 | export const sayHelloAction = createAction("SayHelloAction", undefined); -------------------------------------------------------------------------------- /App/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Dispatch } from "redux"; 4 | import ContentPage from "./Components/ContentPage/ContentPage"; 5 | import BaseComponent from "./Components/BaseComponent"; 6 | import { loadContent } from "./ActionCreators/ContentActionCreators"; 7 | import { StoreState } from "./Store/StoreState"; 8 | 9 | require("./Global/Styles/global.less"); 10 | 11 | interface IAppProps { 12 | loadContent?: () => void; 13 | } 14 | 15 | @connect(undefined, mapDispatchToProps) 16 | class App extends BaseComponent { 17 | doRender(): React.ReactElement<{}> { 18 | return (
19 | 20 |
); 21 | } 22 | 23 | componentDidMount(): void { 24 | this.props.loadContent(); 25 | } 26 | }; 27 | 28 | function mapDispatchToProps(dispatch: Dispatch<{}>): IAppProps { 29 | return { 30 | loadContent: () => dispatch(loadContent()), 31 | }; 32 | } 33 | 34 | export default App; -------------------------------------------------------------------------------- /App/Components/BaseComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Base component which wraps render function in a try catch structure 5 | * Any child components who extends from this component will get protection when 6 | * Exception thrown, so protect component life cycle. 7 | */ 8 | abstract class BaseComponent extends React.Component { 9 | render(): React.ReactElement<{}> { 10 | let result: React.ReactElement<{}>; 11 | try { 12 | result = this.doRender(); 13 | } catch (error) { 14 | this.logError(error); 15 | result = null; 16 | } 17 | 18 | return result; 19 | } 20 | 21 | /** 22 | * Abstract method to be overriden by child component which will do real 23 | * render work as usual react component 24 | */ 25 | abstract doRender(): React.ReactElement<{}>; 26 | 27 | /** 28 | * API to log exception 29 | */ 30 | logError(error: Error): void { 31 | /* tslint:disable */ 32 | const componentName: string = (this as any)._reactInternalInstance._currentElement.type.name; 33 | const componentDetail: string = (this as any)._reactInternalInstance._currentElement.type.toString(); 34 | let propsString = ""; 35 | for (let propName in this.props) { 36 | propsString += " " + propName; 37 | } 38 | /* tslint:enable */ 39 | 40 | console.error(error, {Component: componentName, ComponentDetail: componentDetail, PropList: propsString}); 41 | console.error("A component (" + componentName + ") had an error during render. " + 42 | "Please fix this immediately, even if you don't own this component. " + 43 | "This message is designed to be annoying so that the problem is addressed."); 44 | } 45 | }; 46 | 47 | export default BaseComponent; 48 | -------------------------------------------------------------------------------- /App/Components/Common/Button/Button.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../Global/Styles/variables.less"; 2 | 3 | .button { 4 | outline: none; 5 | border: 0; 6 | padding: 10px 10px; 7 | background-color: @green-foreground-color; 8 | color: @primary-backround-color; 9 | font-size: 15px; 10 | cursor: pointer; 11 | width: 250px; 12 | 13 | &:hover { 14 | background-color: @green-background-color; 15 | color: @green-foreground-color; 16 | } 17 | } -------------------------------------------------------------------------------- /App/Components/Common/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../BaseComponent"; 3 | 4 | // tslint:disable-next-line:no-any 5 | const styles: any = require("./Button.module.less"); 6 | 7 | interface IButtonProps { 8 | onClick: () => void; 9 | styleOverride?: string; 10 | }; 11 | 12 | export default class Button extends BaseComponent { 13 | doRender(): React.ReactElement<{}> { 14 | let buttonStyle = styles.button; 15 | 16 | if (this.props.styleOverride) { 17 | buttonStyle += " " + this.props.styleOverride; 18 | } 19 | 20 | return (); 25 | } 26 | }; -------------------------------------------------------------------------------- /App/Components/ContentPage/BodyWrapper/BodyWrapper.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../Global/Styles/variables.less"; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | padding: 20px; 7 | } 8 | 9 | .title { 10 | font-size: 24px; 11 | padding-top: 10px; 12 | padding-bottom: 10px; 13 | font-weight: bold; 14 | text-align: center; 15 | } 16 | 17 | .summary { 18 | font-size: 20px; 19 | } 20 | 21 | .summaryTitle { 22 | composes: summary; 23 | font-weight: bold; 24 | margin-bottom: 10px; 25 | } 26 | 27 | .logo { 28 | width: 150px; 29 | margin-top: 30px; 30 | align-self: center; 31 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/BodyWrapper/BodyWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../BaseComponent"; 3 | 4 | // tslint:disable-next-line:no-any 5 | const styles: any = require("./BodyWrapper.module.less"); 6 | 7 | interface IBodyWrapperProps extends React.Props<{}> { 8 | title: string; 9 | summary: string; 10 | }; 11 | 12 | export default class BodyWrapper extends BaseComponent { 13 | doRender(): React.ReactElement<{}> { 14 | return (
15 |
{this.props.title}
16 |
Summary
17 |
{this.props.summary}
18 | ("headerlogo.png")} alt="logo" className={styles.logo} /> 19 | {this.props.children} 20 |
); 21 | } 22 | }; -------------------------------------------------------------------------------- /App/Components/ContentPage/BodyWrapper/resources/headerlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepaar/typescript-webpack-react-redux-boilerplate/c7f023a4c4e44d1c2e4e9b83159e82789dc7e09b/App/Components/ContentPage/BodyWrapper/resources/headerlogo.png -------------------------------------------------------------------------------- /App/Components/ContentPage/ContentPage.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../Global/Styles/variables.less"; 2 | 3 | .container { 4 | height: 100%; 5 | } 6 | 7 | .hello { 8 | margin-top: 30px; 9 | margin-bottom: 30px; 10 | font-size: 20px; 11 | color: @green-foreground-color; 12 | text-align: center; 13 | align-self: center; 14 | } 15 | 16 | .message { 17 | margin-top: 10px; 18 | font-weight: bold; 19 | } 20 | 21 | .subreddits { 22 | display: flex; 23 | flex-direction: column; 24 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/ContentPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Dispatch } from "redux"; 4 | import { sayHello } from "./../../ActionCreators/HelloCountActionCreators"; 5 | import { fetchPosts } from "./../../ActionCreators/SubredditsActionCreators"; 6 | import { StoreState } from "./../../Store/StoreState"; 7 | import BaseComponent from "./../BaseComponent"; 8 | import Button from "./../Common/Button/Button"; 9 | import Header from "./Header/Header"; 10 | import BodyWrapper from "./BodyWrapper/BodyWrapper"; 11 | import SubredditChooser from "./SubredditChooser/SubredditChooser"; 12 | import Posts from "./Posts/Posts"; 13 | 14 | // tslint:disable-next-line:no-any 15 | const styles: any = require("./ContentPage.module.less"); 16 | 17 | interface IContentPageProps { 18 | bodyTitle?: string; 19 | bodySummary?: string; 20 | sayHelloCount?: number; 21 | selectedSubreddit?: Subreddit; 22 | 23 | sayHello?: () => void; 24 | fetchSubreddit?: (subreddit: string) => void; 25 | } 26 | 27 | @connect(mapStateToProps, mapDispatchToProps) 28 | class ContentPage extends BaseComponent { 29 | doRender(): React.ReactElement<{}> { 30 | return (
31 |
32 | 33 |
34 | 35 |
You said hello {this.props.sayHelloCount} time(s)
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 |
); 44 | } 45 | } 46 | 47 | function mapStateToProps(state: StoreState): IContentPageProps { 48 | return { 49 | bodyTitle: state.content.title, 50 | bodySummary: state.content.summary, 51 | sayHelloCount: state.helloCount.count, 52 | selectedSubreddit: state.subreddits.items[state.subreddits.selectedSubreddit], 53 | }; 54 | } 55 | 56 | function mapDispatchToProps(dispatch: Dispatch<{}>): IContentPageProps { 57 | return { 58 | sayHello: () => dispatch(sayHello()), 59 | fetchSubreddit: (subreddit: string) => dispatch(fetchPosts(subreddit)), 60 | }; 61 | } 62 | 63 | export default ContentPage; -------------------------------------------------------------------------------- /App/Components/ContentPage/Header/Header.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../Global/Styles/variables.less"; 2 | 3 | .container { 4 | color: @green-foreground-color; 5 | border: 1px; 6 | background-color: @green-background-color; 7 | text-align: center; 8 | height: 40px; 9 | line-height: 40px; 10 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../BaseComponent"; 3 | 4 | // tslint:disable-next-line:no-any 5 | const styles: any = require("./Header.module.less"); 6 | 7 | interface IHeaderProps { 8 | isActive: boolean; 9 | title: string; 10 | }; 11 | 12 | export default class Header extends BaseComponent { 13 | doRender(): React.ReactElement<{}> { 14 | if (!this.props.isActive) { 15 | return null; 16 | } 17 | 18 | return (
{this.props.title}
); 19 | } 20 | }; -------------------------------------------------------------------------------- /App/Components/ContentPage/Posts/Posts.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../Global/Styles/variables.less"; 2 | 3 | .container { 4 | margin-top: 20px; 5 | } 6 | 7 | .title { 8 | font-weight: bold; 9 | font-size: 35px; 10 | margin-bottom: 10px; 11 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/Posts/Posts.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../BaseComponent"; 3 | import PostsList from "./PostsList/PostsList"; 4 | 5 | // tslint:disable-next-line:no-any 6 | const styles: any = require("./Posts.module.less"); 7 | 8 | interface IPostsProps { 9 | subreddit: Subreddit; 10 | }; 11 | 12 | export default class Posts extends BaseComponent { 13 | doRender(): React.ReactElement<{}> { 14 | if (!this.props.subreddit) { 15 | return null; 16 | } 17 | 18 | return (
19 |
{this.props.subreddit.name}
20 | {this.props.subreddit.isLoading && 21 |
Loading...
22 | } 23 | {this.props.subreddit.isError && 24 |
Sorry, error occured
25 | } 26 | {!this.props.subreddit.isLoading && !this.props.subreddit.isError && 27 | 28 | } 29 |
); 30 | } 31 | }; -------------------------------------------------------------------------------- /App/Components/ContentPage/Posts/PostsList/PostsList.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../../Global/Styles/variables.less"; 2 | 3 | .container { 4 | font-size: 20px; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: flex-start; 8 | } 9 | 10 | .post { 11 | padding: 10px 0; 12 | color: @green-foreground-color; 13 | 14 | > a { 15 | text-decoration: none; 16 | color: @green-foreground-color; 17 | } 18 | 19 | &:hover { 20 | background-color: @green-background-color; 21 | } 22 | 23 | &:nth-of-type(even) { 24 | align-self: flex-end; 25 | } 26 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/Posts/PostsList/PostsList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../../BaseComponent"; 3 | 4 | // tslint:disable-next-line:no-any 5 | const styles: any = require("./PostsList.module.less"); 6 | 7 | interface IPostsListProps { 8 | posts: ReadonlyArray; 9 | }; 10 | 11 | export default class PostsList extends BaseComponent { 12 | doRender(): React.ReactElement<{}> { 13 | if (this.props.posts.length === 0) { 14 | return (
No posts
); 15 | } 16 | 17 | const posts = this.props.posts.slice(0, 5).map((post, index) => { 18 | return (
19 | {index + 1}.) {post.title} 20 | |▴{post.ups}▾{post.downs} 21 |
); 22 | }); 23 | 24 | return (
{posts}
); 25 | } 26 | }; -------------------------------------------------------------------------------- /App/Components/ContentPage/SubredditChooser/SubredditChooser.module.less: -------------------------------------------------------------------------------- 1 | @import (reference) "../../../Global/Styles/variables.less"; 2 | 3 | .chooseSubreddit { 4 | font-size: 20px; 5 | font-weight: bold; 6 | } 7 | 8 | .subredditButton { 9 | width: auto; 10 | flex-grow: 1; 11 | margin: 10px; 12 | } 13 | 14 | .subredditButtons { 15 | display: flex; 16 | } -------------------------------------------------------------------------------- /App/Components/ContentPage/SubredditChooser/SubredditChooser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import BaseComponent from "./../../BaseComponent"; 3 | import Button from "./../../Common/Button/Button"; 4 | 5 | // tslint:disable-next-line:no-any 6 | const styles: any = require("./SubredditChooser.module.less"); 7 | 8 | interface ISubredditChooserProps { 9 | fetchSubreddit: (subreddit: string) => void; 10 | }; 11 | 12 | export default class SubredditChooser extends BaseComponent { 13 | doRender(): React.ReactElement<{}> { 14 | return (
15 |
Choose subreddit
16 |
17 | 18 | 19 | 20 |
21 |
); 22 | } 23 | }; -------------------------------------------------------------------------------- /App/DataLayer/SubredditsProvider.ts: -------------------------------------------------------------------------------- 1 | // Handle errors in real world app 2 | export function fetchPosts(subreddit: string): Promise { 3 | return fetch(`https://www.reddit.com/r/${subreddit}.json`) 4 | .then((response: Response) => response.json()) 5 | .then((json: any) => json.data.children.map((child: any) => { 6 | return { 7 | id: child.data.id, 8 | title: child.data.title, 9 | url: child.data.url, 10 | ups: child.data.ups, 11 | downs: child.data.downs, 12 | }; 13 | }) as Post[]); 14 | } -------------------------------------------------------------------------------- /App/Global/Styles/global.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /App/Global/Styles/variables.less: -------------------------------------------------------------------------------- 1 | @primary-backround-color: #E6F2E9; 2 | @green-foreground-color: #025204; 3 | @green-background-color: #AFDEB0; 4 | @primary-red: red; -------------------------------------------------------------------------------- /App/Index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import App from "./App"; 5 | import { configureStore } from "./Store/CreateStore"; 6 | 7 | const store = configureStore(); 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root")); -------------------------------------------------------------------------------- /App/Models/Post.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Post { 2 | readonly id: string; 3 | readonly title: string; 4 | readonly url: string; 5 | readonly ups: number; 6 | readonly downs: number; 7 | } -------------------------------------------------------------------------------- /App/Models/Subreddit.d..ts: -------------------------------------------------------------------------------- 1 | declare interface Subreddit { 2 | readonly name: string; 3 | readonly isLoading: boolean; 4 | readonly isError: boolean; 5 | readonly posts: ReadonlyArray; 6 | } -------------------------------------------------------------------------------- /App/Reducers/ContentReducer.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions"; 2 | import { ContentLoadActionPayload, contentLoadAction } from "./../Actions/ContentLoadAction"; 3 | import { ContentState } from "./../Store/State/ContentState"; 4 | 5 | const initialState: ContentState = { 6 | title: "", 7 | summary: "", 8 | }; 9 | 10 | export default handleActions({ 11 | [contentLoadAction.toString()]: (state, action) => { 12 | return { 13 | title: action.payload.title ? action.payload.title.toUpperCase() : "", 14 | summary: action.payload.summary, 15 | }; 16 | }, 17 | }, initialState); -------------------------------------------------------------------------------- /App/Reducers/HelloCountReducer.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions"; 2 | import { sayHelloAction } from "./../Actions/SayHelloAction"; 3 | import { HelloCountState } from "./../Store/State/HelloCountState"; 4 | 5 | const initialState: HelloCountState = { 6 | count: 0, 7 | }; 8 | 9 | export default handleActions({ 10 | [sayHelloAction.toString()]: (state, action) => { 11 | return { 12 | count: state.count + 1, 13 | }; 14 | }, 15 | }, initialState); -------------------------------------------------------------------------------- /App/Reducers/RootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer } from "redux"; 2 | import helloCountReducer from "./HelloCountReducer"; 3 | import contentReducer from "./ContentReducer"; 4 | import subredditsReducer from "./SubredditsReducer"; 5 | import { StoreState } from "./../Store/StoreState"; 6 | 7 | const rootReducer: Reducer = combineReducers({ 8 | helloCount: helloCountReducer, 9 | content: contentReducer, 10 | subreddits: subredditsReducer, 11 | }); 12 | 13 | export default rootReducer; -------------------------------------------------------------------------------- /App/Reducers/SubredditsReducer.ts: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions"; 2 | import { getSubredditPostsStartAction, getSubredditPostsSuccessAction, getSubredditPostsErrorAction, 3 | GetSubredditPostsStartActionPayload, GetSubredditPostsSuccessActionPayload, 4 | GetSubredditPostsErrorActionPayload } from "./../Actions/GetSubredditPostsActions"; 5 | import { SubredditsState } from "./../Store/State/SubredditsState"; 6 | 7 | const initialState: SubredditsState = { 8 | items: {}, 9 | selectedSubreddit: undefined, 10 | }; 11 | 12 | export default handleActions({ 13 | [getSubredditPostsStartAction.toString()]: (state, action: ReduxActions.Action) => { 14 | return { 15 | selectedSubreddit: action.payload.subreddit, 16 | items: { 17 | ...state.items, 18 | [action.payload.subreddit]: { 19 | name: action.payload.subreddit, 20 | isLoading: true, 21 | isError: false, 22 | posts: [], 23 | }, 24 | }, 25 | }; 26 | }, 27 | 28 | [getSubredditPostsSuccessAction.toString()]: (state, action: ReduxActions.Action) => { 29 | const updatedItem = { 30 | ...state.items[action.payload.subreddit], 31 | isLoading: false, 32 | posts: action.payload.posts, 33 | }; 34 | 35 | return { 36 | ...state, 37 | items: { 38 | ...state.items, 39 | [action.payload.subreddit]: updatedItem, 40 | }, 41 | }; 42 | }, 43 | 44 | [getSubredditPostsErrorAction.toString()]: (state, action: GetSubredditPostsErrorActionPayload) => { 45 | const errorItem = { 46 | ...state.items[action.subreddit], 47 | isLoading: false, 48 | isError: true, 49 | posts: [], 50 | }; 51 | 52 | return { 53 | ...state, 54 | items: { 55 | ...state.items, 56 | [action.subreddit]: errorItem, 57 | }, 58 | }; 59 | }, 60 | 61 | }, initialState); -------------------------------------------------------------------------------- /App/Store/CreateStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, Store, Middleware, Reducer } from "redux"; 2 | import { createLogger } from "redux-logger"; 3 | import thunkMiddleware from "redux-thunk"; 4 | import rootReducer from "./../Reducers/RootReducer"; 5 | import { StoreState } from "./StoreState"; 6 | 7 | export function configureStore(initialState?: StoreState): Store { 8 | const middlewares: Middleware[] = [ 9 | thunkMiddleware, 10 | createLogger(), 11 | ]; 12 | 13 | const composeEnhancers = 14 | DEBUG && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 15 | 16 | const store = createStore(rootReducer, initialState, composeEnhancers( 17 | applyMiddleware(...middlewares), 18 | )); 19 | 20 | if (module.hot) { 21 | module.hot.accept("./../Reducers/RootReducer", () => { 22 | store.replaceReducer((require("./../Reducers/RootReducer") as Reducer)); 23 | }); 24 | } 25 | 26 | return store; 27 | } -------------------------------------------------------------------------------- /App/Store/State/ContentState.ts: -------------------------------------------------------------------------------- 1 | export interface ContentState { 2 | readonly title: string; 3 | readonly summary: string; 4 | } -------------------------------------------------------------------------------- /App/Store/State/HelloCountState.ts: -------------------------------------------------------------------------------- 1 | export interface HelloCountState { 2 | readonly count: number; 3 | } -------------------------------------------------------------------------------- /App/Store/State/SubredditsState.ts: -------------------------------------------------------------------------------- 1 | export interface SubredditsState { 2 | readonly selectedSubreddit: string; 3 | readonly items: {[subreddit: string]: Subreddit}; 4 | } -------------------------------------------------------------------------------- /App/Store/StoreState.ts: -------------------------------------------------------------------------------- 1 | import { HelloCountState } from "./State/HelloCountState"; 2 | import { ContentState } from "./State/ContentState"; 3 | import { SubredditsState } from "./State/SubredditsState"; 4 | 5 | export interface StoreState { 6 | readonly helloCount: HelloCountState; 7 | readonly content: ContentState; 8 | readonly subreddits: SubredditsState; 9 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Petar Paar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-webpack-react-redux-boilerplate 2 | The goal of this repo is to help you start when combining Typescript with React and Redux. 3 | LESS in combination of CSS modules is used for styling components. For running, building and bundling there is powerful Webpack. 4 | 5 | ## This boilerplate combines 6 | * [Typescript 2](https://github.com/Microsoft/TypeScript) 7 | * [Babel](https://babeljs.io/) 8 | * [React](https://github.com/facebook/react) 9 | * [Redux](https://github.com/reactjs/redux) 10 | * [LESS](https://github.com/less/less.js) 11 | * [CSS modules](https://github.com/css-modules/css-modules) 12 | * [Webpack 2](https://webpack.js.org/) 13 | * [Tslint](https://palantir.github.io/tslint/) 14 | 15 | ## Getting started 16 | * run **npm install** to download dependencies 17 | * run **npm start** to build and start webpack-dev-server 18 | * open **http://localhost:3333/** 19 | 20 | ## Build options 21 | * **npm run build** for single build (creates files and index.html in build/ folder) 22 | * **npm start** to build and start webpack-dev-server 23 | 24 | ## More information 25 | * [Why typescript?](https://medium.com/@delveeng/why-we-love-typescript-bec2df88d6c2) 26 | * [Flux implementation](https://medium.com/@delveeng/how-we-use-the-flux-architecture-in-delve-effc551f8fbc) 27 | * [Why CSS modules?](https://medium.com/@delveeng/how-we-build-css-in-office-delve-3440ae67bae9) 28 | * [Why Webpack?](https://medium.com/@delveeng/how-we-bundle-delve-using-webpack-c13d9c9624c) 29 | -------------------------------------------------------------------------------- /babel/babelhelpers.js: -------------------------------------------------------------------------------- 1 | // https://babeljs.io/docs/plugins/external-helpers/ 2 | 3 | (function (global) { 4 | var babelHelpers = global.babelHelpers = {}; 5 | babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 6 | return typeof obj; 7 | } : function (obj) { 8 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 9 | }; 10 | 11 | babelHelpers.jsx = function () { 12 | var REACT_ELEMENT_TYPE = typeof Symbol === "function" && Symbol.for && Symbol.for("react.element") || 0xeac7; 13 | return function createRawReactElement(type, props, key, children) { 14 | var defaultProps = type && type.defaultProps; 15 | var childrenLength = arguments.length - 3; 16 | 17 | if (!props && childrenLength !== 0) { 18 | props = {}; 19 | } 20 | 21 | if (props && defaultProps) { 22 | for (var propName in defaultProps) { 23 | if (props[propName] === void 0) { 24 | props[propName] = defaultProps[propName]; 25 | } 26 | } 27 | } else if (!props) { 28 | props = defaultProps || {}; 29 | } 30 | 31 | if (childrenLength === 1) { 32 | props.children = children; 33 | } else if (childrenLength > 1) { 34 | var childArray = Array(childrenLength); 35 | 36 | for (var i = 0; i < childrenLength; i++) { 37 | childArray[i] = arguments[i + 3]; 38 | } 39 | 40 | props.children = childArray; 41 | } 42 | 43 | return { 44 | $$typeof: REACT_ELEMENT_TYPE, 45 | type: type, 46 | key: key === undefined ? null : '' + key, 47 | ref: null, 48 | props: props, 49 | _owner: null 50 | }; 51 | }; 52 | }(); 53 | 54 | babelHelpers.asyncIterator = function (iterable) { 55 | if (typeof Symbol === "function") { 56 | if (Symbol.asyncIterator) { 57 | var method = iterable[Symbol.asyncIterator]; 58 | if (method != null) return method.call(iterable); 59 | } 60 | 61 | if (Symbol.iterator) { 62 | return iterable[Symbol.iterator](); 63 | } 64 | } 65 | 66 | throw new TypeError("Object is not async iterable"); 67 | }; 68 | 69 | babelHelpers.asyncGenerator = function () { 70 | function AwaitValue(value) { 71 | this.value = value; 72 | } 73 | 74 | function AsyncGenerator(gen) { 75 | var front, back; 76 | 77 | function send(key, arg) { 78 | return new Promise(function (resolve, reject) { 79 | var request = { 80 | key: key, 81 | arg: arg, 82 | resolve: resolve, 83 | reject: reject, 84 | next: null 85 | }; 86 | 87 | if (back) { 88 | back = back.next = request; 89 | } else { 90 | front = back = request; 91 | resume(key, arg); 92 | } 93 | }); 94 | } 95 | 96 | function resume(key, arg) { 97 | try { 98 | var result = gen[key](arg); 99 | var value = result.value; 100 | 101 | if (value instanceof AwaitValue) { 102 | Promise.resolve(value.value).then(function (arg) { 103 | resume("next", arg); 104 | }, function (arg) { 105 | resume("throw", arg); 106 | }); 107 | } else { 108 | settle(result.done ? "return" : "normal", result.value); 109 | } 110 | } catch (err) { 111 | settle("throw", err); 112 | } 113 | } 114 | 115 | function settle(type, value) { 116 | switch (type) { 117 | case "return": 118 | front.resolve({ 119 | value: value, 120 | done: true 121 | }); 122 | break; 123 | 124 | case "throw": 125 | front.reject(value); 126 | break; 127 | 128 | default: 129 | front.resolve({ 130 | value: value, 131 | done: false 132 | }); 133 | break; 134 | } 135 | 136 | front = front.next; 137 | 138 | if (front) { 139 | resume(front.key, front.arg); 140 | } else { 141 | back = null; 142 | } 143 | } 144 | 145 | this._invoke = send; 146 | 147 | if (typeof gen.return !== "function") { 148 | this.return = undefined; 149 | } 150 | } 151 | 152 | if (typeof Symbol === "function" && Symbol.asyncIterator) { 153 | AsyncGenerator.prototype[Symbol.asyncIterator] = function () { 154 | return this; 155 | }; 156 | } 157 | 158 | AsyncGenerator.prototype.next = function (arg) { 159 | return this._invoke("next", arg); 160 | }; 161 | 162 | AsyncGenerator.prototype.throw = function (arg) { 163 | return this._invoke("throw", arg); 164 | }; 165 | 166 | AsyncGenerator.prototype.return = function (arg) { 167 | return this._invoke("return", arg); 168 | }; 169 | 170 | return { 171 | wrap: function (fn) { 172 | return function () { 173 | return new AsyncGenerator(fn.apply(this, arguments)); 174 | }; 175 | }, 176 | await: function (value) { 177 | return new AwaitValue(value); 178 | } 179 | }; 180 | }(); 181 | 182 | babelHelpers.asyncGeneratorDelegate = function (inner, awaitWrap) { 183 | var iter = {}, 184 | waiting = false; 185 | 186 | function pump(key, value) { 187 | waiting = true; 188 | value = new Promise(function (resolve) { 189 | resolve(inner[key](value)); 190 | }); 191 | return { 192 | done: false, 193 | value: awaitWrap(value) 194 | }; 195 | } 196 | 197 | ; 198 | 199 | if (typeof Symbol === "function" && Symbol.iterator) { 200 | iter[Symbol.iterator] = function () { 201 | return this; 202 | }; 203 | } 204 | 205 | iter.next = function (value) { 206 | if (waiting) { 207 | waiting = false; 208 | return value; 209 | } 210 | 211 | return pump("next", value); 212 | }; 213 | 214 | if (typeof inner.throw === "function") { 215 | iter.throw = function (value) { 216 | if (waiting) { 217 | waiting = false; 218 | throw value; 219 | } 220 | 221 | return pump("throw", value); 222 | }; 223 | } 224 | 225 | if (typeof inner.return === "function") { 226 | iter.return = function (value) { 227 | return pump("return", value); 228 | }; 229 | } 230 | 231 | return iter; 232 | }; 233 | 234 | babelHelpers.asyncToGenerator = function (fn) { 235 | return function () { 236 | var gen = fn.apply(this, arguments); 237 | return new Promise(function (resolve, reject) { 238 | function step(key, arg) { 239 | try { 240 | var info = gen[key](arg); 241 | var value = info.value; 242 | } catch (error) { 243 | reject(error); 244 | return; 245 | } 246 | 247 | if (info.done) { 248 | resolve(value); 249 | } else { 250 | return Promise.resolve(value).then(function (value) { 251 | step("next", value); 252 | }, function (err) { 253 | step("throw", err); 254 | }); 255 | } 256 | } 257 | 258 | return step("next"); 259 | }); 260 | }; 261 | }; 262 | 263 | babelHelpers.classCallCheck = function (instance, Constructor) { 264 | if (!(instance instanceof Constructor)) { 265 | throw new TypeError("Cannot call a class as a function"); 266 | } 267 | }; 268 | 269 | babelHelpers.createClass = function () { 270 | function defineProperties(target, props) { 271 | for (var i = 0; i < props.length; i++) { 272 | var descriptor = props[i]; 273 | descriptor.enumerable = descriptor.enumerable || false; 274 | descriptor.configurable = true; 275 | if ("value" in descriptor) descriptor.writable = true; 276 | Object.defineProperty(target, descriptor.key, descriptor); 277 | } 278 | } 279 | 280 | return function (Constructor, protoProps, staticProps) { 281 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 282 | if (staticProps) defineProperties(Constructor, staticProps); 283 | return Constructor; 284 | }; 285 | }(); 286 | 287 | babelHelpers.defineEnumerableProperties = function (obj, descs) { 288 | for (var key in descs) { 289 | var desc = descs[key]; 290 | desc.configurable = desc.enumerable = true; 291 | if ("value" in desc) desc.writable = true; 292 | Object.defineProperty(obj, key, desc); 293 | } 294 | 295 | return obj; 296 | }; 297 | 298 | babelHelpers.defaults = function (obj, defaults) { 299 | var keys = Object.getOwnPropertyNames(defaults); 300 | 301 | for (var i = 0; i < keys.length; i++) { 302 | var key = keys[i]; 303 | var value = Object.getOwnPropertyDescriptor(defaults, key); 304 | 305 | if (value && value.configurable && obj[key] === undefined) { 306 | Object.defineProperty(obj, key, value); 307 | } 308 | } 309 | 310 | return obj; 311 | }; 312 | 313 | babelHelpers.defineProperty = function (obj, key, value) { 314 | if (key in obj) { 315 | Object.defineProperty(obj, key, { 316 | value: value, 317 | enumerable: true, 318 | configurable: true, 319 | writable: true 320 | }); 321 | } else { 322 | obj[key] = value; 323 | } 324 | 325 | return obj; 326 | }; 327 | 328 | babelHelpers.extends = Object.assign || function (target) { 329 | for (var i = 1; i < arguments.length; i++) { 330 | var source = arguments[i]; 331 | 332 | for (var key in source) { 333 | if (Object.prototype.hasOwnProperty.call(source, key)) { 334 | target[key] = source[key]; 335 | } 336 | } 337 | } 338 | 339 | return target; 340 | }; 341 | 342 | babelHelpers.get = function get(object, property, receiver) { 343 | if (object === null) object = Function.prototype; 344 | var desc = Object.getOwnPropertyDescriptor(object, property); 345 | 346 | if (desc === undefined) { 347 | var parent = Object.getPrototypeOf(object); 348 | 349 | if (parent === null) { 350 | return undefined; 351 | } else { 352 | return get(parent, property, receiver); 353 | } 354 | } else if ("value" in desc) { 355 | return desc.value; 356 | } else { 357 | var getter = desc.get; 358 | 359 | if (getter === undefined) { 360 | return undefined; 361 | } 362 | 363 | return getter.call(receiver); 364 | } 365 | }; 366 | 367 | babelHelpers.inherits = function (subClass, superClass) { 368 | if (typeof superClass !== "function" && superClass !== null) { 369 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 370 | } 371 | 372 | subClass.prototype = Object.create(superClass && superClass.prototype, { 373 | constructor: { 374 | value: subClass, 375 | enumerable: false, 376 | writable: true, 377 | configurable: true 378 | } 379 | }); 380 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 381 | }; 382 | 383 | babelHelpers.instanceof = function (left, right) { 384 | if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { 385 | return right[Symbol.hasInstance](left); 386 | } else { 387 | return left instanceof right; 388 | } 389 | }; 390 | 391 | babelHelpers.interopRequireDefault = function (obj) { 392 | return obj && obj.__esModule ? obj : { 393 | default: obj 394 | }; 395 | }; 396 | 397 | babelHelpers.interopRequireWildcard = function (obj) { 398 | if (obj && obj.__esModule) { 399 | return obj; 400 | } else { 401 | var newObj = {}; 402 | 403 | if (obj != null) { 404 | for (var key in obj) { 405 | if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; 406 | } 407 | } 408 | 409 | newObj.default = obj; 410 | return newObj; 411 | } 412 | }; 413 | 414 | babelHelpers.newArrowCheck = function (innerThis, boundThis) { 415 | if (innerThis !== boundThis) { 416 | throw new TypeError("Cannot instantiate an arrow function"); 417 | } 418 | }; 419 | 420 | babelHelpers.objectDestructuringEmpty = function (obj) { 421 | if (obj == null) throw new TypeError("Cannot destructure undefined"); 422 | }; 423 | 424 | babelHelpers.objectWithoutProperties = function (obj, keys) { 425 | var target = {}; 426 | 427 | for (var i in obj) { 428 | if (keys.indexOf(i) >= 0) continue; 429 | if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; 430 | target[i] = obj[i]; 431 | } 432 | 433 | return target; 434 | }; 435 | 436 | babelHelpers.possibleConstructorReturn = function (self, call) { 437 | if (!self) { 438 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 439 | } 440 | 441 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 442 | }; 443 | 444 | babelHelpers.selfGlobal = typeof global === "undefined" ? self : global; 445 | 446 | babelHelpers.set = function set(object, property, value, receiver) { 447 | var desc = Object.getOwnPropertyDescriptor(object, property); 448 | 449 | if (desc === undefined) { 450 | var parent = Object.getPrototypeOf(object); 451 | 452 | if (parent !== null) { 453 | set(parent, property, value, receiver); 454 | } 455 | } else if ("value" in desc && desc.writable) { 456 | desc.value = value; 457 | } else { 458 | var setter = desc.set; 459 | 460 | if (setter !== undefined) { 461 | setter.call(receiver, value); 462 | } 463 | } 464 | 465 | return value; 466 | }; 467 | 468 | babelHelpers.slicedToArray = function () { 469 | function sliceIterator(arr, i) { 470 | var _arr = []; 471 | var _n = true; 472 | var _d = false; 473 | var _e = undefined; 474 | 475 | try { 476 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 477 | _arr.push(_s.value); 478 | 479 | if (i && _arr.length === i) break; 480 | } 481 | } catch (err) { 482 | _d = true; 483 | _e = err; 484 | } finally { 485 | try { 486 | if (!_n && _i["return"]) _i["return"](); 487 | } finally { 488 | if (_d) throw _e; 489 | } 490 | } 491 | 492 | return _arr; 493 | } 494 | 495 | return function (arr, i) { 496 | if (Array.isArray(arr)) { 497 | return arr; 498 | } else if (Symbol.iterator in Object(arr)) { 499 | return sliceIterator(arr, i); 500 | } else { 501 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 502 | } 503 | }; 504 | }(); 505 | 506 | babelHelpers.slicedToArrayLoose = function (arr, i) { 507 | if (Array.isArray(arr)) { 508 | return arr; 509 | } else if (Symbol.iterator in Object(arr)) { 510 | var _arr = []; 511 | 512 | for (var _iterator = arr[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) { 513 | _arr.push(_step.value); 514 | 515 | if (i && _arr.length === i) break; 516 | } 517 | 518 | return _arr; 519 | } else { 520 | throw new TypeError("Invalid attempt to destructure non-iterable instance"); 521 | } 522 | }; 523 | 524 | babelHelpers.taggedTemplateLiteral = function (strings, raw) { 525 | return Object.freeze(Object.defineProperties(strings, { 526 | raw: { 527 | value: Object.freeze(raw) 528 | } 529 | })); 530 | }; 531 | 532 | babelHelpers.taggedTemplateLiteralLoose = function (strings, raw) { 533 | strings.raw = raw; 534 | return strings; 535 | }; 536 | 537 | babelHelpers.temporalRef = function (val, name, undef) { 538 | if (val === undef) { 539 | throw new ReferenceError(name + " is not defined - temporal dead zone"); 540 | } else { 541 | return val; 542 | } 543 | }; 544 | 545 | babelHelpers.temporalUndefined = {}; 546 | 547 | babelHelpers.toArray = function (arr) { 548 | return Array.isArray(arr) ? arr : Array.from(arr); 549 | }; 550 | 551 | babelHelpers.toConsumableArray = function (arr) { 552 | if (Array.isArray(arr)) { 553 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 554 | 555 | return arr2; 556 | } else { 557 | return Array.from(arr); 558 | } 559 | }; 560 | })(typeof global === "undefined" ? self : global); 561 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Typescript-Webpack-React-Flux/Redux boilerplate 4 | 5 | 6 |
7 |
8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-webpack-react-flux-boilerplate", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/pepaar/typescript-webpack-react-flux-boilerplate.git" 8 | }, 9 | "scripts": { 10 | "start": "node server.js", 11 | "build": "webpack" 12 | }, 13 | "dependencies": { 14 | "@types/react": "15.0.28", 15 | "@types/react-dom": "15.5.0", 16 | "@types/react-redux": "4.4.38", 17 | "@types/redux-actions": "1.2.6", 18 | "@types/redux-logger": "3.0.0", 19 | "@types/webpack-env": "1.13.0", 20 | "react": "15.6.0", 21 | "react-dom": "15.6.0", 22 | "react-redux": "5.0.5", 23 | "redux": "3.6.0", 24 | "redux-actions": "2.0.3", 25 | "redux-logger": "3.0.6", 26 | "redux-thunk": "2.2.0" 27 | }, 28 | "devDependencies": { 29 | "awesome-typescript-loader": "3.1.3", 30 | "babel-cli": "6.24.1", 31 | "babel-core": "6.25.0", 32 | "babel-loader": "7.0.0", 33 | "babel-plugin-external-helpers": "6.22.0", 34 | "babel-polyfill": "6.23.0", 35 | "babel-preset-es2015": "6.24.1", 36 | "babel-preset-es2015-loose": "8.0.0", 37 | "css-loader": "0.28.4", 38 | "file-loader": "0.11.2", 39 | "html-webpack-plugin": "2.28.0", 40 | "less": "2.7.2", 41 | "less-loader": "4.0.4", 42 | "node-libs-browser": "2.0.0", 43 | "react-hot-loader": "1.3.1", 44 | "style-loader": "0.18.2", 45 | "tslint": "4.5.1", 46 | "tslint-loader": "3.5.3", 47 | "typescript": "2.3.4", 48 | "webpack": "2.6.1", 49 | "webpack-config": "7.0.0", 50 | "webpack-dev-server": "2.4.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var WebpackDevServer = require('webpack-dev-server'); 4 | var config = require('./webpack.dev.config'); 5 | 6 | var compiler = webpack(config); 7 | 8 | var server = new WebpackDevServer(compiler, { 9 | hot: true, 10 | // display no info to console (only warnings and errors) 11 | noInfo: false, 12 | publicPath: config.output.publicPath, 13 | stats: { 14 | // With console colors 15 | colors: true, 16 | // add the hash of the compilation 17 | hash: true, 18 | // add webpack version information 19 | version: false, 20 | // add timing information 21 | timings: true, 22 | // add assets information 23 | assets: false, 24 | // add chunk information 25 | chunks: false, 26 | // add built modules information to chunk information 27 | chunkModules: false, 28 | // add built modules information 29 | modules: false, 30 | // add also information about cached (not built) modules 31 | cached: false, 32 | // add information about the reasons why modules are included 33 | reasons: false, 34 | // add the source code of modules 35 | source: false, 36 | // add details to errors (like resolving log) 37 | errorDetails: true, 38 | // add the origins of chunks and chunk merging info 39 | chunkOrigins: false, 40 | // Add messages from child loaders 41 | children: false 42 | } 43 | }); 44 | 45 | server.listen(3333, 'localhost', function (err) { 46 | if (err) { 47 | console.log(err); 48 | return; 49 | } 50 | 51 | console.log("Listening at http://localhost:3333. Please wait, I'm building things for you..."); 52 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "sourceMap": true, 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "experimentalDecorators": true, 8 | 9 | "typeRoots": [ 10 | "Models", "typings", "node_modules/@types" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "sourceMap": true, 5 | "jsx": "react", 6 | "moduleResolution": "node", 7 | "experimentalDecorators": true, 8 | 9 | "typeRoots": [ 10 | "Models", "typings", "node_modules/@types" 11 | ] 12 | }, 13 | "files": [ 14 | ] 15 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "eofline": false, 9 | "max-line-length": [true, 160], 10 | "member-access": false, 11 | "array-type": [false], 12 | "ordered-imports": [false], 13 | "object-literal-sort-keys": false, 14 | "no-var-requires": false, 15 | "interface-name": [ 16 | false 17 | ] 18 | }, 19 | "rulesDirectory": [] 20 | } -------------------------------------------------------------------------------- /typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 3 | } -------------------------------------------------------------------------------- /typings/react-redux.d.ts: -------------------------------------------------------------------------------- 1 | import "react-redux"; 2 | 3 | declare module "react-redux" { 4 | // To enable usage of decorator 5 | // https://github.com/reactjs/react-redux/pull/541#issuecomment-269197189 6 | export function connect( 7 | mapStateToProps?: MapStateToProps | MapStateToPropsFactory, 8 | mapDispatchToProps?: MapDispatchToProps | MapDispatchToPropsFactory, 9 | mergeProps?: MergeProps, 10 | options?: Options 11 | ): ClassDecorator; 12 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | 5 | var nodeModulesPath = path.join(__dirname, 'node_modules'); 6 | var isProduction = process.env.NODE_ENV == "production"; 7 | 8 | var config = { 9 | entry: { 10 | vendors: [ 11 | 'react', 12 | 'react-dom', 13 | 'redux', 14 | 'react-redux', 15 | 'redux-thunk', 16 | 'redux-logger', 17 | 'babel-polyfill', 18 | path.join(__dirname, 'babel', 'babelhelpers.js') 19 | ], 20 | app: [ 21 | path.join(__dirname, 'App', 'Index.tsx') 22 | ] 23 | }, 24 | 25 | resolve: { 26 | extensions: ['.tsx', '.ts', '.js', '.less', '.css'], 27 | modules: ["node_modules", "resources"], 28 | }, 29 | 30 | output: { 31 | path: path.join(__dirname, 'build'), 32 | filename: '[name]_[chunkhash].js' 33 | }, 34 | 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.tsx?$/, 39 | enforce: 'pre', 40 | loader: 'tslint-loader', 41 | options: { emitErrors: true } 42 | }, 43 | { 44 | test: /\.tsx?$/, 45 | loaders: ["babel-loader?cacheDirectory", "awesome-typescript-loader?tsconfig=tsconfig.webpack.json&useCache=true"] 46 | }, 47 | { 48 | test: /\.css$/, 49 | loaders: ["style-loader", "css-loader?minimize"] 50 | }, 51 | { 52 | test: /\.less$/, 53 | exclude: /\.module\.less$/, 54 | loaders: ["style-loader", "css-loader?minimize", "less-loader?compress"] 55 | }, 56 | { 57 | test: /\.module\.less$/, 58 | loaders: ["style-loader", "css-loader?minimize&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]", "less-loader?-compress"] 59 | }, 60 | { 61 | test: /\.(jpg|png|woff|eot|ttf|svg|gif)$/, 62 | loader: "file-loader?name=[name]_[hash].[ext]" 63 | } 64 | ] 65 | }, 66 | 67 | plugins: [ 68 | new webpack.optimize.CommonsChunkPlugin({ name: 'vendors', filename: 'vendors_[hash].js' }), 69 | new HtmlWebpackPlugin({ 70 | template: 'index.html' 71 | }), 72 | new webpack.DefinePlugin({ 73 | DEBUG: true 74 | }) 75 | ] 76 | }; 77 | 78 | if (isProduction) { 79 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({ 80 | compress: { 81 | warnings: false 82 | } 83 | })); 84 | config.plugins.push(new webpack.DefinePlugin({ 85 | 'process.env': {NODE_ENV: '"production"'}, 86 | DEBUG: false 87 | })); 88 | } 89 | 90 | module.exports = config; 91 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | // This config is extented from webpack.config.js. We use it for development with webpack-dev-server and autoreload/refresh 2 | 3 | var webpack = require('webpack'); 4 | var { Config } = require('webpack-config'); 5 | var path = require("path"); 6 | 7 | var mainConfig = new Config().extend("webpack.config"); 8 | mainConfig.module.rules = []; 9 | 10 | var devConfigExtension = { 11 | entry: { 12 | app: [ 13 | // We are using next two entries for hot-reload 14 | 'webpack-dev-server/client?http://localhost:3333', 15 | 'webpack/hot/only-dev-server', 16 | ] 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | publicPath: "http://localhost:3333/" 22 | }, 23 | 24 | // more options here: http://webpack.github.io/docs/configuration.html#devtool 25 | devtool: 'eval-source-map', 26 | 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.tsx?$/, 31 | enforce: 'pre', 32 | loader: 'tslint-loader', 33 | options: { emitErrors: true }, 34 | include: path.join(__dirname, "App") 35 | }, 36 | { 37 | test: /\.tsx?$/, 38 | loaders: ["react-hot-loader", "babel-loader?cacheDirectory", "awesome-typescript-loader?tsconfig=tsconfig.webpack.json&useCache=true"] 39 | }, 40 | { 41 | test: /\.css$/, 42 | loaders: ["style-loader", "css-loader"] 43 | }, 44 | { 45 | test: /\.less$/, 46 | exclude: /\.module\.less$/, 47 | loaders: ["style-loader", "css-loader", "less-loader"] 48 | }, 49 | { 50 | test: /\.module\.less$/, 51 | loaders: ["style-loader", "css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]", "less-loader"] 52 | }, 53 | { 54 | test: /\.(jpg|png|woff|eot|ttf|svg|gif)$/, 55 | loader: "file-loader?name=[name].[ext]" 56 | } 57 | ] 58 | }, 59 | 60 | plugins: [ 61 | // Used for hot-reload 62 | new webpack.HotModuleReplacementPlugin(), 63 | new webpack.DefinePlugin({ 64 | DEBUG: true 65 | }) 66 | ] 67 | }; 68 | 69 | module.exports = mainConfig.merge(devConfigExtension); --------------------------------------------------------------------------------