├── .gitignore ├── src ├── components │ ├── IssueCommentList.scss │ ├── IssueNewHeader.scss │ ├── IssueDescription.scss │ ├── IssueList.scss │ ├── IssueListItem.scss │ ├── IssueDescription.js │ ├── IssueListHeader.scss │ ├── IssueCommentList.js │ ├── IssueListItem.js │ ├── IssueList.js │ ├── SelectModal.js │ ├── IssueCommentForm.scss │ ├── IssueCommentListItem.scss │ ├── IssueNewHeader.js │ ├── IssueDetailHeader.scss │ ├── IssueCommentListItem.js │ ├── IssueCommentForm.js │ ├── IssueListHeader.js │ └── IssueDetailHeader.js ├── containers │ ├── IssueListContainer.scss │ ├── IssueNewContainer.scss │ ├── IssueDetailContainer.scss │ ├── IssueContainer.js │ ├── IssueNewContainer.js │ ├── IssueListContainer.js │ └── IssueDetailContainer.js ├── lib │ ├── records │ │ ├── IssueListManager.js │ │ ├── IssueManager.js │ │ ├── IssueNewManager.js │ │ ├── IssueDetailManager.js │ │ ├── User.js │ │ ├── Label.js │ │ ├── Comment.js │ │ └── Issue.js │ ├── constants │ │ └── EndPoints.js │ └── utils │ │ └── nl2br.js ├── reducers │ ├── issueApp.js │ └── issue.js ├── stores │ └── configureIssueStore.js ├── entries │ └── issue.js └── actions │ ├── issueNew.js │ ├── issue.js │ └── issueDetail.js ├── server.babel.js ├── public └── index.html ├── .babelrc ├── README.md ├── webpack-dev-server.babel.js ├── webpack.config.babel.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /src/components/IssueCommentList.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | margin-top: 10px; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/IssueNewHeader.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/IssueListContainer.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | margin: 50px 50px; 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/IssueNewContainer.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | margin: 50px 50px; 3 | } 4 | -------------------------------------------------------------------------------- /src/containers/IssueDetailContainer.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | margin: 50px 100px; 3 | } 4 | 5 | .main { 6 | padding: 30px 100px; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/records/IssueListManager.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable' 2 | 3 | const _IssueListManager = Record({ 4 | loading: false, 5 | }) 6 | 7 | export default class IssueListManager extends _IssueListManager { 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/records/IssueManager.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable' 2 | 3 | const _IssueManager = Record({ 4 | users: new List(), 5 | labels: new List(), 6 | }) 7 | 8 | export default class IssueManager extends _IssueManager { 9 | } 10 | -------------------------------------------------------------------------------- /src/reducers/issueApp.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer } from 'react-router-redux' 3 | 4 | import issue from './issue' 5 | 6 | export default combineReducers({ 7 | issue, 8 | routing: routerReducer, 9 | }) 10 | -------------------------------------------------------------------------------- /src/lib/records/IssueNewManager.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable' 2 | import Issue from './Issue' 3 | 4 | const _IssueNewManager = Record({ 5 | loading: false, 6 | issue: new Issue(), 7 | }) 8 | 9 | export default class IssueNewManager extends _IssueNewManager { 10 | } 11 | -------------------------------------------------------------------------------- /src/components/IssueDescription.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | border: 1px solid DarkGray; 3 | border-radius: 3px; 4 | margin-bottom: 30px; 5 | } 6 | 7 | .header { 8 | background-color: LightGray; 9 | padding: 10px 10px; 10 | font-weight: 600; 11 | } 12 | 13 | .main { 14 | padding: 10px 10px; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/records/IssueDetailManager.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable' 2 | 3 | const _IssueDetailManager = Record({ 4 | isTitleEditing: false, 5 | loading: false, 6 | showUsersModal: false, 7 | showLabelsModal: false, 8 | }) 9 | 10 | export default class IssueDetailManager extends _IssueDetailManager { 11 | } 12 | -------------------------------------------------------------------------------- /server.babel.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import path from 'path' 3 | 4 | const app = express() 5 | const root = path.join(__dirname, 'public') 6 | 7 | app.use('/', express.static(root)) 8 | 9 | app.get('*', (req, res, _next) => { 10 | res.sendFile('index.html', { root }) 11 | }) 12 | 13 | app.listen(process.env.PORT || 8000) 14 | -------------------------------------------------------------------------------- /src/lib/records/User.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable' 2 | 3 | const _User = Record({ 4 | id: null, 5 | name: '', 6 | }) 7 | 8 | export default class User extends _User { 9 | static fromJS(user = {}) { 10 | return (new this).merge({ 11 | id: parseInt(user.id), 12 | name: user.name, 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/constants/EndPoints.js: -------------------------------------------------------------------------------- 1 | export const API_HOSTS = { 2 | REACT_REDUX_SOKUSHU_API: 'https://react-redux-sokushu-api.herokuapp.com', 3 | } 4 | 5 | const END_POINTS = { 6 | ISSUES: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/issues`, 7 | USERS: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/users`, 8 | LABELS: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/labels`, 9 | } 10 | 11 | export default END_POINTS 12 | -------------------------------------------------------------------------------- /src/lib/records/Label.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable' 2 | 3 | const _Label = Record({ 4 | id: null, 5 | name: '', 6 | color_code: '', 7 | }) 8 | 9 | export default class Label extends _Label { 10 | static fromJS(label = {}) { 11 | return (new this).merge({ 12 | id: parseInt(label.id), 13 | name: label.name, 14 | color_code: label.color_code, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/utils/nl2br.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const newlineRegex = /(\r\n|\n\r|\r|\n)/g 3 | 4 | export default function nl2br(str) { 5 | if (typeof str !== 'string') { 6 | return '' 7 | } 8 | 9 | return str.split(newlineRegex).map((line, index) => { 10 | if (line.match(newlineRegex)) { 11 | return React.createElement('br', { key: index }) 12 | } else { 13 | return line 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Issues PRACTICE 9 | 10 | 11 | 12 |

Issues index.html PRICTICE

13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-3", "react"], 3 | "plugins": [ 4 | ["react-transform", { 5 | "transforms": [{ 6 | "transform": "react-transform-hmr", 7 | "imports": ["react"], 8 | "locals": ["module"], 9 | }, { 10 | "transform": "react-transform-catch-errors", 11 | "imports": [ 12 | "react", 13 | "redbox-react", 14 | ], 15 | }], 16 | }], 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/components/IssueList.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | border-bottom: 1px solid DarkGray; 3 | } 4 | 5 | .header { 6 | display: flex; 7 | background: LightGray; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .base-row { 13 | align-items: center; 14 | text-align: center; 15 | } 16 | 17 | .row { 18 | composes: base-row; 19 | flex: 1; 20 | } 21 | 22 | .row-2 { 23 | composes: base-row; 24 | flex: 2; 25 | } 26 | 27 | .row-3 { 28 | composes: base-row; 29 | flex: 3; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/records/Comment.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable' 2 | 3 | const _Comment = Record({ 4 | id: null, 5 | userName: '', 6 | content: '', 7 | created: '', 8 | updated: '', 9 | }) 10 | 11 | export default class Comment extends _Comment { 12 | static fromJS(comment = {}) { 13 | return (new this).merge({ 14 | id: comment.id, 15 | userName: comment.user_name || comment.userName, 16 | content: comment.content, 17 | created: comment.created, 18 | updated: comment.updated, 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/stores/configureIssueStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunk from 'redux-thunk' 3 | import reducer from '../reducers/issueApp' 4 | 5 | 6 | export default function configureStore(_initialData) { 7 | const store = createStore( 8 | reducer, 9 | undefined, 10 | // Middlewares 11 | compose( 12 | applyMiddleware(thunk) 13 | ) 14 | ) 15 | 16 | module.hot.accept('../reducers/issueApp', () => { 17 | store.replaceReducer(require('../reducers/issueApp').default) 18 | }) 19 | 20 | return store 21 | } 22 | -------------------------------------------------------------------------------- /src/components/IssueListItem.scss: -------------------------------------------------------------------------------- 1 | .outer { 2 | :hover { 3 | background: Azure; 4 | cursor: pointer; 5 | } 6 | } 7 | 8 | .base { 9 | display: flex; 10 | flex-flow: row nowrap; 11 | justify-content: center; 12 | border-bottom: 1px solid LightGray; 13 | padding: 10px 0; 14 | } 15 | 16 | .base-row { 17 | align-items: center; 18 | text-align: center; 19 | } 20 | 21 | .row { 22 | composes: base-row; 23 | flex: 1; 24 | } 25 | 26 | .row-2 { 27 | composes: base-row; 28 | flex: 2; 29 | } 30 | 31 | .row-3 { 32 | composes: base-row; 33 | flex: 3; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/IssueDescription.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import CSSModules from 'react-css-modules' 3 | 4 | import styles from './IssueDescription.scss' 5 | 6 | class IssueDescription extends Component { 7 | render() { 8 | const { issue } = this.props 9 | 10 | return ( 11 |
12 |
13 | issue description 14 |
15 |
16 | { issue.content } 17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export default CSSModules(IssueDescription, styles) 24 | -------------------------------------------------------------------------------- /src/components/IssueListHeader.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | display: flex; 3 | justify-content: center; 4 | margin-bottom: 10px; 5 | } 6 | 7 | .left { 8 | margin-right: auto; 9 | } 10 | 11 | .right { 12 | padding: 0 13 | } 14 | 15 | .item { 16 | margin-right: 10px; 17 | cursor: pointer; 18 | text-decoration: none; 19 | 20 | &:hover { 21 | text-decoration: underline; 22 | } 23 | } 24 | 25 | .modal-item { 26 | text-decoration: none; 27 | list-style: none; 28 | margin: 4px; 29 | cursor: pointer; 30 | } 31 | 32 | .modal-item-check { 33 | margin-left: 8px; 34 | color: red; 35 | } 36 | 37 | .modal-close-btn { 38 | text-align: right; 39 | cursor: pointer; 40 | margin: 0 16px; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-sokushu-practice 2 | 3 | このリポジトリのソースは[React + Reduxを使ったWebアプリケーション開発速習会@Wantedly](http://wantedly.connpass.com/event/33168/)のハンズオン用のものです。 4 | 5 | ソースの解説などは[Qiita記事](http://qiita.com/shimpeiws/private/df31e2d70cc67c68115d)をご覧ください。 6 | 7 | 完成済みのアプリケーションは[shimpeiws/react-redux-sokushu](https://github.com/shimpeiws/react-redux-sokushu)にあります。 8 | 9 | ## 起動 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | サーバの起動 16 | 17 | ``` 18 | npm run serve 19 | ``` 20 | 21 | webpack dev serverの起動 22 | 23 | ``` 24 | npm run webpack:serve 25 | ``` 26 | 27 | `localhost:8000` にアクセスし画面が表示されればOKです。 28 | 29 | ![スクリーンショット 2016-06-09 14.31.23.png](https://qiita-image-store.s3.amazonaws.com/0/29637/095942c1-0ab9-41d9-4af0-145af9df488f.png "スクリーンショット 2016-06-09 14.31.23.png") 30 | -------------------------------------------------------------------------------- /src/components/IssueCommentList.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import CSSModules from 'react-css-modules' 3 | 4 | import IssueCommentListItem from './IssueCommentListItem' 5 | 6 | import styles from './IssueCommentList.scss' 7 | 8 | class IssueCommentList extends Component { 9 | render() { 10 | const { comments } = this.props 11 | 12 | return ( 13 |
14 | { 15 | comments.map((comment) => { 16 | return () 22 | }) 23 | } 24 |
25 | ) 26 | } 27 | } 28 | 29 | export default CSSModules(IssueCommentList, styles) 30 | -------------------------------------------------------------------------------- /src/containers/IssueContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | 6 | import { 7 | findInitialData, 8 | } from '../actions/issue' 9 | 10 | class IssueContainer extends Component { 11 | componentDidMount() { 12 | this.init() 13 | } 14 | 15 | init() { 16 | this.props.findInitialData() 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 | { this.props.children } 23 |
24 | ) 25 | } 26 | } 27 | 28 | const mapStateToProps = (state, ownProps) => { 29 | return {} 30 | } 31 | 32 | const mapDispatchToProps = (dispatch) => { 33 | return bindActionCreators({ 34 | findInitialData, 35 | }, dispatch) 36 | } 37 | 38 | export default connect( 39 | mapStateToProps, 40 | mapDispatchToProps 41 | )(IssueContainer) 42 | -------------------------------------------------------------------------------- /webpack-dev-server.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import express from 'express' 3 | import _ from 'lodash' 4 | import config, { serverPort, serverURI } from './webpack.config.babel.js' 5 | 6 | config.plugins.unshift( 7 | new webpack.HotModuleReplacementPlugin(), 8 | new webpack.NoErrorsPlugin() 9 | ) 10 | 11 | const webpackDevModules = [ 12 | `webpack-hot-middleware/client?path=${serverURI}/__webpack_hmr`, 13 | ] 14 | 15 | _.each(config.entry, (file, name) => { 16 | config.entry[name] = webpackDevModules.concat([file]) 17 | }) 18 | 19 | const compiler = webpack(config) 20 | const app = express() 21 | 22 | app.use(require('webpack-dev-middleware')(compiler, { 23 | noInfo: true, 24 | publicPath: config.output.publicPath, 25 | })) 26 | 27 | app.use(require('webpack-hot-middleware')(compiler)) 28 | 29 | app.listen(serverPort, 'localhost', (err) => { 30 | if (err) { 31 | cosole.log(err) 32 | return 33 | } 34 | 35 | console.log(`Dev server is listening at ${serverURI}`) 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/IssueListItem.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import CSSModules from 'react-css-modules' 3 | 4 | import styles from './IssueListItem.scss' 5 | 6 | class IssueListItem extends Component { 7 | onClickRow(e) { 8 | this.props.onClickRow(this.props.issue) 9 | } 10 | 11 | render() { 12 | const { issue } = this.props 13 | 14 | return( 15 |
16 |
17 |
{issue.id}
18 |
19 | {issue.title} 20 |
21 |
{issue.status}
22 |
23 | { 24 | issue.assignee.id ? (issue.assignee.name) : ("-") 25 | } 26 |
27 |
{issue.created}
28 |
{issue.updated}
29 |
30 |
31 | ) 32 | } 33 | } 34 | 35 | export default CSSModules(IssueListItem, styles) 36 | -------------------------------------------------------------------------------- /src/components/IssueList.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import CSSModules from 'react-css-modules' 3 | 4 | import IssueListItem from './IssueListItem' 5 | import styles from './IssueList.scss' 6 | 7 | class IssueList extends Component { 8 | render() { 9 | const { issues } = this.props 10 | 11 | return( 12 |
13 |
14 |
id
15 |
title
16 |
status
17 |
assignee
18 |
created
19 |
updated
20 |
21 | { 22 | issues.map((issue) => { 23 | return () 28 | }) 29 | } 30 |
31 | ) 32 | } 33 | } 34 | 35 | export default CSSModules(IssueList, styles) 36 | -------------------------------------------------------------------------------- /src/components/SelectModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import Modal from 'react-modal' 5 | 6 | class SelectModal extends Component { 7 | 8 | render() { 9 | return 12 | {this.props.children} 13 | 14 | } 15 | } 16 | 17 | const styles = { 18 | overlay: { 19 | position: 'fixed', 20 | top: 0, 21 | left: 0, 22 | right: 0, 23 | bottom: 0, 24 | zIndex: 3000, 25 | backgroundColor: 'rgba(0, 0, 0, 0.4)', 26 | }, 27 | content: { 28 | position: 'absolute', 29 | top: '50%', 30 | left: '50%', 31 | right: 'auto', 32 | bottom: 'auto', 33 | width: '880px', 34 | height: '681px', 35 | margin: '-340px -440px 0', 36 | padding: '0', 37 | border: 'none', 38 | background: '#fff', 39 | overflow: 'auto', 40 | WebkitOverflowScrolling: 'touch', 41 | borderRadius: '4px', 42 | outline: 'none', 43 | boxSizing: 'border-box', 44 | WebkitFontSmoothing: 'antialiased', 45 | } 46 | } 47 | 48 | export default SelectModal 49 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 4 | 5 | export const serverPort = 8080 6 | export const serverURI = `http://localhost:${serverPort}` 7 | 8 | export default { 9 | entry: { 10 | issue: './src/entries/issue.js', 11 | }, 12 | 13 | output: { 14 | path: path.join(__dirname, 'public/js'), 15 | filename: '[name].js', 16 | publicPath: `${serverURI}/assets/build/`, 17 | }, 18 | 19 | resolve: { 20 | extensions: ['', '.js', '.jsx'], 21 | }, 22 | 23 | plugins: [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | new webpack.NoErrorsPlugin(), 26 | new ExtractTextPlugin('[name].css'), 27 | ], 28 | 29 | module: { 30 | loaders: [ 31 | { 32 | test: /\.jsx?$/, 33 | loader: 'babel', 34 | exclude: /node_modules/, 35 | }, 36 | { 37 | test: /.s?css$/, 38 | loaders: [ 39 | 'style', 40 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]', 41 | 'resolve-url', 42 | 'sass', 43 | ], 44 | exclude: /node_modules/, 45 | }, 46 | ], 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/entries/issue.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { createStore, combineReducers, applyMiddleware } from 'redux' 3 | import ReactDOM from 'react-dom' 4 | import { Router, Route, browserHistory, Link, IndexRoute } from 'react-router' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import { Provider } from 'react-redux' 7 | import thunk from 'redux-thunk' 8 | import _ from 'lodash' 9 | import 'babel-polyfill' 10 | 11 | 12 | import IssueContainer from '../containers/IssueContainer' 13 | import IssueListContainer from '../containers/IssueListContainer' 14 | import IssueDetailContainer from '../containers/IssueDetailContainer' 15 | import IssueNewContainer from '../containers/IssueNewContainer' 16 | import configureStore from '../stores/configureIssueStore' 17 | 18 | const store = configureStore() 19 | const history = syncHistoryWithStore(browserHistory, store) 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | , 31 | document.getElementById('content') 32 | ) 33 | -------------------------------------------------------------------------------- /src/components/IssueCommentForm.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | border: 1px solid DarkGray; 3 | border-radius: 3px; 4 | margin-top: 30px; 5 | } 6 | 7 | .header { 8 | background-color: LightGray; 9 | padding: 10px 10px; 10 | display: flex; 11 | align-items: center; 12 | } 13 | 14 | .input-label { 15 | font-weight: 600; 16 | } 17 | 18 | .user-input { 19 | margin-left: 30px; 20 | } 21 | 22 | .main { 23 | background-color: white; 24 | padding: 10px 10px; 25 | } 26 | 27 | .comment-text { 28 | margin-top: 10px; 29 | width: 100%; 30 | height: 150px; 31 | rows: 10px; 32 | font-size: 15px; 33 | } 34 | 35 | .footer { 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: flex-end; 39 | margin-bottom: 15px; 40 | } 41 | 42 | .close-issue-button { 43 | padding: 0 15px; 44 | height: 30px; 45 | line-height: 30px; 46 | font-size: 15px; 47 | border: 1px solid DarkGray; 48 | border-radius: 3px; 49 | text-align: center; 50 | margin-right: 15px; 51 | background: #eee; 52 | cursor: pointer; 53 | display: block; 54 | } 55 | 56 | .comment-button { 57 | composes: close-issue-button; 58 | background: green; 59 | color: white; 60 | } 61 | 62 | textarea, input { 63 | width: 100%; 64 | border-radius: 3px; 65 | background-color: #fff; 66 | border: solid 1px #e3e3e3; 67 | padding: 10px; 68 | box-sizing: border-box; 69 | display: block; 70 | 71 | &:disabled { 72 | background: #eee; 73 | } 74 | } 75 | 76 | input { 77 | padding: 5px; 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/records/Issue.js: -------------------------------------------------------------------------------- 1 | import { List, Record } from 'immutable' 2 | 3 | import Comment from './Comment' 4 | import User from './User' 5 | import Label from './Label' 6 | 7 | export const STATE = { 8 | CLOSE: 'close', 9 | OPEN: 'open', 10 | } 11 | 12 | const _Issue = Record({ 13 | id: null, 14 | title: '', 15 | status: STATE.CLOSE, 16 | created: '', 17 | updated: '', 18 | comments: new List(), 19 | content: '', 20 | assignee: new User(), 21 | labels: new List(), 22 | }) 23 | 24 | export default class Issue extends _Issue { 25 | static fromJS(issue = {}) { 26 | let comments = new List() 27 | let labels = new List() 28 | 29 | if (issue.comments) { 30 | comments = new List(issue.comments.map((comment) => { 31 | return Comment.fromJS(comment) 32 | })) 33 | } 34 | 35 | if (issue.labels) { 36 | labels = new List(issue.labels.map((label) => { 37 | return Label.fromJS(label) 38 | })) 39 | } 40 | 41 | return (new this).merge({ 42 | id: parseInt(issue.id), 43 | title: issue.title, 44 | status: issue.status, 45 | created: issue.created, 46 | updated: issue.updated, 47 | content: issue.content, 48 | comments, 49 | labels, 50 | assignee: issue.assignee ? User.fromJS(issue.assignee) : new User(), 51 | }) 52 | } 53 | 54 | isValidTitle() { 55 | return this.title.length > 0 56 | } 57 | 58 | isValidContent() { 59 | return this.content.length > 0 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/IssueCommentListItem.scss: -------------------------------------------------------------------------------- 1 | .base { 2 | border: 1px solid DarkGray; 3 | border-radius: 3px; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .header { 8 | background-color: LightGray; 9 | padding: 10px 10px; 10 | display: flex; 11 | } 12 | 13 | .header-name { 14 | flex: 4; 15 | font-weight: 600; 16 | } 17 | 18 | .header-date { 19 | flex: 12; 20 | font-size: 15px; 21 | font-weight: 400; 22 | } 23 | 24 | .actions { 25 | flex: 2; 26 | text-align: right; 27 | } 28 | 29 | .header-icon { 30 | cursor: pointer; 31 | margin-left: 15px; 32 | } 33 | 34 | .main { 35 | background-color: white; 36 | padding: 10px 10px; 37 | overflow: hidden; 38 | 39 | textarea { 40 | width: 100%; 41 | font-size: 14px; 42 | border-radius: 3px; 43 | background-color: #fff; 44 | border: solid 1px #e3e3e3; 45 | padding: 10px; 46 | box-sizing: border-box; 47 | display: block; 48 | } 49 | } 50 | 51 | .buttons { 52 | width: 300px; 53 | margin-top: 10px; 54 | float: right; 55 | text-align: right; 56 | overflow: hidden; 57 | } 58 | 59 | .cancel-button { 60 | float: right; 61 | padding: 0 15px; 62 | height: 30px; 63 | line-height: 30px; 64 | font-size: 15px; 65 | border: 1px solid DarkGray; 66 | border-radius: 3px; 67 | text-align: center; 68 | margin-left: 15px; 69 | background: red; 70 | color: white; 71 | cursor: pointer; 72 | display: block; 73 | } 74 | 75 | .comment-button { 76 | composes: cancel-button; 77 | background: green; 78 | } 79 | -------------------------------------------------------------------------------- /src/components/IssueNewHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import CSSModules from 'react-css-modules' 3 | 4 | import styles from './IssueNewHeader.scss' 5 | 6 | class IssueNewHeader extends Component { 7 | constructor(props) { 8 | super(props) 9 | } 10 | 11 | onChangeTitle(e) { 12 | this.props.onChangeTitle(e.target.value) 13 | } 14 | 15 | onChangeContent(e) { 16 | this.props.onChangeContent(e.target.value) 17 | } 18 | 19 | onCreateIssue(e) { 20 | this.props.onCreateIssue() 21 | } 22 | 23 | render() { 24 | const {issue} = this.props.issueNewManager 25 | return ( 26 |
27 |
28 | title: 29 | 34 | 35 |
36 |
37 | content: 38 |