)
30 | }
31 |
32 | return (
33 |
34 | {generateKindString(homeRecommendation.get('kind'))} |
35 | {homeRecommendationNode} |
36 | {friendlyTimeWithLineBreak(homeRecommendation.createdAt)} |
37 |
38 |
39 |
42 |
45 |
48 |
49 | {/**/}
53 |
54 | |
55 |
56 | )
57 | })
58 |
59 | return (
60 |
61 |
62 |
63 | 类型 |
64 | 推荐对象 |
65 | 推荐于 |
66 | 操作 |
67 |
68 |
69 | {rows}
70 |
71 | )
72 | }
73 | }
74 |
75 | function generateKindString(kind) {
76 | switch (kind) {
77 | case "user":
78 | return "用户"
79 | default:
80 | return ""
81 | }
82 | }
83 |
84 | const styles = {
85 | kindCell: {
86 | minWidth: '100px'
87 | },
88 | userCell: {
89 | paddingBottom: '12px'
90 | },
91 | timeCell: {
92 | minWidth: '100px'
93 | },
94 | reorder: {
95 | ':hover': {
96 | cursor: "move"
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/components/homeRecommendations/UserHomeRecommendation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { browserHistory } from 'react-router'
4 |
5 | @Radium
6 | export default class UserHomeRecommendation extends React.Component {
7 | static propTypes = {
8 | homeRecommendation: React.PropTypes.object.isRequired
9 | }
10 |
11 | render() {
12 | const { homeRecommendation } = this.props
13 | const user = homeRecommendation.get('user')
14 | const backgroundImageUrl = user.get('userCardBackgroundImage')
15 | ? user.get('userCardBackgroundImage').url()
16 | : user.get('avatar').url()
17 |
18 | return (
19 |
20 |
{user.get('achievements')}
21 |
browserHistory.push(`/user/${user.id}`)}
22 | style={[
23 | {backgroundImage: 'linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8)), url(' + backgroundImageUrl + ')'},
24 | styles.backgroundImageWap
25 | ]}>
26 |
27 |
{user.get('name')}
28 |
{user.get('title')}
29 |
30 |
31 |
32 | {user.get('tags').join(', ')}
33 | {user.get('answeredQuestionsCount')} 问答
34 |
35 |
36 | )
37 | }
38 | }
39 |
40 | const styles = {
41 | achievements: {
42 | fontWeight: 'bold'
43 | },
44 | backgroundImageWap: {
45 | marginTop: '5px',
46 | width: '250px',
47 | height: '125px',
48 | color: 'white',
49 | backgroundPosition: 'center',
50 | backgroundRepeat: 'no-repeat',
51 | backgroundSize: 'cover',
52 | position: 'relative',
53 | ':hover': {
54 | cursor: 'pointer'
55 | }
56 | },
57 | nameTitleWap: {
58 | position: 'absolute',
59 | bottom: '10px',
60 | left: '12px'
61 | },
62 | name: {
63 | fontWeight: '1000',
64 | fontSize: '15px'
65 | },
66 | title: {
67 | fontSize: '12px',
68 | marginTop: '3px'
69 | },
70 | bottomWap: {
71 | marginTop: '5px',
72 | fontSize: '12px',
73 | color: 'gray'
74 | },
75 | tags: {},
76 | answeredQuestionsCount: {
77 | marginLeft: '20px'
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/components/homeRecommendations/UserSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 |
4 | @Radium
5 | export default class UserSelector extends React.Component {
6 | static propTypes = {
7 | users: React.PropTypes.array.isRequired,
8 | selectUser: React.PropTypes.func.isRequired
9 | }
10 |
11 | render() {
12 | const { users, selectUser } = this.props
13 | const userNodes = users.map((user, index, array) => {
14 | return (
15 |
19 |
20 |
.url()})
22 |
{user.get('name')}
23 |
24 |
25 |
28 |
29 |
30 | )
31 | })
32 |
33 | return (
34 | {userNodes}
35 | )
36 | }
37 | }
38 |
39 | const styles = {
40 | wap: {
41 | maxHeight: '200px',
42 | overflowY: 'auto'
43 | },
44 | row: {
45 | ':hover': {
46 | backgroundColor: '#EEE'
47 | }
48 | },
49 | userName: {
50 | marginLeft: '5px'
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/components/invitationCodes/InvitationCodesList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { browserHistory, Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak } from '../../filters'
5 |
6 | @Radium
7 | export default class InvitationCodesList extends React.Component {
8 | static propTypes = {
9 | invitationCodes: React.PropTypes.array.isRequired,
10 | openModal: React.PropTypes.func.isRequired
11 | }
12 |
13 | render() {
14 | const { invitationCodes, openModal } = this.props
15 | const rows = invitationCodes.map(code => {
16 | return (
17 |
18 | {code.get('code')} |
19 | {generateCodeKind(code)} |
20 | {generateCodeCreator(code)} |
21 | {code.get('sended') ? () : null} |
22 | {friendlyTimeWithLineBreak(code.get('sendedAt'))} |
23 | {code.get('sendedTo')} |
24 | {friendlyTimeWithLineBreak(code.createdAt)} |
25 | {friendlyTimeWithLineBreak(code.get('usedAt'))} |
26 |
27 | {code.get('user')
28 | ? {code.get('user').get('name')}
29 | : null}
30 | |
31 |
32 |
33 |
34 |
37 |
38 |
39 | |
40 |
41 | )
42 | })
43 |
44 | return (
45 |
46 |
47 |
48 | 邀请码 |
49 | 类型 |
50 | 创建者 |
51 | 已发送 |
52 | 发送于 |
53 | 发送对象 |
54 | 创建于 |
55 | 使用于 |
56 | 注册用户 |
57 | 操作 |
58 |
59 |
60 | {rows}
61 |
62 | )
63 | }
64 | }
65 |
66 | function generateCodeKind(code) {
67 | let kind = ""
68 |
69 | switch (code.get('kind')) {
70 | case 1:
71 | kind = "提问被回答"
72 | break
73 | case 2:
74 | kind = "用户生成"
75 | break
76 | case 3:
77 | kind = "管理员生成"
78 | break
79 | }
80 |
81 | return kind
82 | }
83 |
84 | function generateCodeCreator(code) {
85 | switch (code.get('kind')) {
86 | case 1:
87 | return "system"
88 | case 2:
89 | return (
90 | {code.get('createdBy').get('name')}
91 | )
92 | case 3:
93 | return "admin"
94 | default:
95 | return ""
96 | }
97 | }
98 |
99 | const styles = {
100 | unusedCodeCell: {
101 | fontWeight: 'bold'
102 | },
103 | usedCodeCell: {
104 | textDecoration: 'line-through',
105 | color: 'lightgray'
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/components/invitationCodes/MarkInvitationCodeSendedModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import BaseModal from '../public/BaseFormModal'
4 |
5 | @Radium
6 | export default class MarkInvitationCodeSendedModal extends React.Component {
7 | state = {
8 | sendedTo: "",
9 | code: null
10 | }
11 |
12 | static propTypes = {
13 | code: React.PropTypes.object,
14 | closeModal: React.PropTypes.func.isRequired,
15 | markSended: React.PropTypes.func.isRequired,
16 | modalIsOpen: React.PropTypes.bool.isRequired
17 | }
18 |
19 | handleSendedToChange = (e) => {
20 | this.setState({sendedTo: e.target.value})
21 | }
22 |
23 | handleSubmit = () => {
24 | const { markSended, closeModal } = this.props
25 |
26 | closeModal()
27 | markSended(this.state.code, this.state.sendedTo)
28 | this.setState({code: null, sendedTo: ""})
29 | }
30 |
31 | componentWillReceiveProps = (nextProps) => {
32 | if (nextProps.code && nextProps.modalIsOpen) {
33 | this.setState({
34 | code: nextProps.code
35 | })
36 | }
37 | }
38 |
39 | render() {
40 | const { closeModal, modalIsOpen } = this.props
41 | return (
42 |
47 |
59 |
60 | )
61 | }
62 | }
63 |
64 | const styles = {}
65 |
--------------------------------------------------------------------------------
/components/notification/BroadcastSystemNotificationsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { friendlyTimeWithLineBreak } from '../../filters'
4 |
5 | @Radium
6 | export default class BroadcastSystemNotificationsList extends React.Component {
7 | static propTypes = {
8 | notifications: React.PropTypes.array.isRequired
9 | }
10 |
11 | render() {
12 | const { notifications } = this.props
13 | const rows = notifications.map(notification => {
14 | return (
15 |
16 |
17 | {notification.get('content')}
18 | |
19 |
20 | {friendlyTimeWithLineBreak(notification.createdAt)}
21 | |
22 |
23 | )
24 | })
25 |
26 | return (
27 |
28 |
29 |
30 | 推送于 |
31 | 内容 |
32 |
33 |
34 | {rows}
35 |
36 | )
37 | }
38 | }
39 |
40 | const styles = {}
--------------------------------------------------------------------------------
/components/public/ActiveNavItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 |
5 | @Radium
6 | export default class ActiveNavItem extends React.Component {
7 | static propTypes = {
8 | path: React.PropTypes.string.isRequired,
9 | text: React.PropTypes.string.isRequired,
10 | currentPath: React.PropTypes.string.isRequired
11 | }
12 |
13 | render() {
14 | return (
15 |
16 | {this.props.text}
17 |
18 | )
19 | }
20 | }
21 |
22 | const styles = {}
23 |
--------------------------------------------------------------------------------
/components/public/BaseFormModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 |
5 | @Radium
6 | export default class BaseFormModal extends React.Component {
7 | static defaultProps = {
8 | submitText: "提交"
9 | }
10 |
11 | static propTypes = {
12 | title: React.PropTypes.string.isRequired,
13 | closeModal: React.PropTypes.func.isRequired,
14 | handleSubmit: React.PropTypes.func.isRequired,
15 | modalIsOpen: React.PropTypes.bool.isRequired,
16 | children: React.PropTypes.element.isRequired,
17 | submitText: React.PropTypes.string
18 | }
19 |
20 | render() {
21 | const { title, closeModal, handleSubmit, submitText, modalIsOpen } = this.props
22 |
23 | return (
24 |
29 |
30 |
31 |
32 |
36 |
{title}
37 |
38 |
39 | {this.props.children}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
49 | }
50 |
51 | const styles = {
52 | overlay: {
53 | zIndex: 10000
54 | },
55 | content: {
56 | position: null,
57 | top: null,
58 | left: null,
59 | right: null,
60 | bottom: null,
61 | border: null,
62 | background: null,
63 | overflow: null,
64 | WebkitOverflowScrolling: null,
65 | borderRadius: null,
66 | padding: null,
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/components/public/BaseInfoModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 |
5 | @Radium
6 | export default class BaseInfoModal extends React.Component {
7 | static propTypes = {
8 | title: React.PropTypes.string.isRequired,
9 | closeModal: React.PropTypes.func.isRequired,
10 | modalIsOpen: React.PropTypes.bool.isRequired,
11 | children: React.PropTypes.element.isRequired,
12 | }
13 |
14 | render() {
15 | const { title, closeModal, modalIsOpen } = this.props
16 |
17 | return (
18 |
23 |
24 |
25 |
26 |
30 |
{title}
31 |
32 |
33 | {this.props.children}
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 | }
43 |
44 | const styles = {
45 | overlay: {
46 | zIndex: 10000
47 | },
48 | content: {
49 | position: null,
50 | top: null,
51 | left: null,
52 | right: null,
53 | bottom: null,
54 | border: null,
55 | background: null,
56 | overflow: null,
57 | WebkitOverflowScrolling: null,
58 | borderRadius: null,
59 | padding: null,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/components/public/Pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import _ from 'lodash'
4 |
5 | @Radium
6 | export default class Pagination extends React.Component {
7 | static defaultProps = {
8 | currentPage: 1,
9 | perPage: 15,
10 | size: 'md'
11 | }
12 |
13 | static propTypes = {
14 | totalCount: React.PropTypes.number.isRequired,
15 | currentPage: React.PropTypes.number.isRequired,
16 | perPage: React.PropTypes.number,
17 | redirect: React.PropTypes.func.isRequired,
18 | size: React.PropTypes.oneOf(['sm', 'md', 'lg'])
19 | }
20 |
21 | get totalPage() {
22 | const { totalCount, perPage } = this.props
23 |
24 | return Math.ceil(totalCount / perPage)
25 | }
26 |
27 | redirectToOtherPage = (page) => {
28 | const { redirect, currentPage } = this.props
29 |
30 | if (page < 1 || page > this.totalPage || page === currentPage) {
31 | return
32 | }
33 |
34 | redirect(page)
35 | }
36 |
37 | render() {
38 | const { redirect, totalCount, currentPage, perPage } = this.props
39 | const totalPage = Math.ceil(totalCount / perPage)
40 | const sizeClassName = this.props.size === 'md' ? '' : `pagination-${this.props.size}`
41 | const pageNumbers = _.range(1, totalPage + 1).map(function (pageNumber) {
42 | return (
43 |
44 | this.redirectToOtherPage(pageNumber)}>
45 | {pageNumber}
46 |
47 |
48 | )
49 | }.bind(this))
50 |
51 | return (
52 |
69 | )
70 | }
71 | }
72 |
73 | const styles = {}
74 |
--------------------------------------------------------------------------------
/components/question/AnswersList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class AnswersList extends React.Component {
8 | static propTypes = {
9 | answeredQuestions: React.PropTypes.array.isRequired,
10 | }
11 |
12 | render() {
13 | const { answeredQuestions } = this.props
14 | const rows = answeredQuestions.map(question => {
15 | const asker = question.get('asker')
16 | const user = question.get('user')
17 |
18 | return (
19 |
20 |
21 |
22 | {asker ? ({asker.get('name')}) : "游客"}
23 |
24 | :
25 | {question.get('title')}
26 | |
27 | {question.get('anonymous') ? : null} |
28 |
29 | {user.get('name')}
30 | :
31 | {truncate(question.get('answer'), 60)}
32 | |
33 | {question.get('commentsCount')} |
34 | {question.get('likesCount')} |
35 | {friendlyTimeWithLineBreak(question.createdAt)} |
36 | {friendlyTimeWithLineBreak(question.get('answeredAt'))} |
37 |
38 | )
39 | })
40 |
41 | return (
42 |
43 |
44 |
45 | 问题 |
46 | 匿名 |
47 | 回答 |
48 | 回复 |
49 | 点赞 |
50 | 提问于 |
51 | 回答于 |
52 |
53 |
54 | {rows}
55 |
56 | )
57 | }
58 | }
59 |
60 | const styles = {
61 | titleCell: {
62 | maxWidth: '200px'
63 | },
64 | answerCell: {
65 | maxWidth: '200px'
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/components/question/AskQuestionModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 | import BaseModal from "../public/BaseFormModal"
5 |
6 | @Radium
7 | export default class AskQuestionModal extends React.Component {
8 | static propTypes = {
9 | askQuestion: React.PropTypes.func.isRequired,
10 | modalIsOpen: React.PropTypes.bool.isRequired,
11 | closeModal: React.PropTypes.func.isRequired
12 | }
13 |
14 | state = {
15 | title: ""
16 | }
17 |
18 | handleTitleChange = (e) => {
19 | this.setState({title: e.target.value})
20 | }
21 |
22 | handleSubmit = () => {
23 | const { askQuestion, closeModal } = this.props
24 |
25 | this.setState({title: ''})
26 | closeModal()
27 | askQuestion(this.state.title)
28 | }
29 |
30 | render() {
31 | const { modalIsOpen, closeModal } = this.props
32 |
33 | return (
34 |
39 |
46 |
47 | )
48 | }
49 | }
50 |
51 | const styles = {}
--------------------------------------------------------------------------------
/components/question/AskedQuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class AskedQuestions extends React.Component {
8 | static propTypes = {
9 | questions: React.PropTypes.array.isRequired,
10 | openQuestionDetailsModal: React.PropTypes.func.isRequired,
11 | deleteQuestion: React.PropTypes.func.isRequired
12 | }
13 |
14 | render() {
15 | const { questions, openQuestionDetailsModal, deleteQuestion } = this.props
16 | const rows = questions.map(question => {
17 | const user = question.get('user')
18 |
19 | return (
20 |
21 |
22 | 向
23 |
24 | {{user.get('name')}}
25 |
26 | 提问:
27 | {question.get('title')}
28 | |
29 | {question.get('anonymous') ? : null} |
30 | {question.get('answered') ? : null} |
31 |
32 | {truncate(question.get('answer'), 60)}
33 | |
34 | {question.get('commentsCount')} |
35 | {question.get('likesCount')} |
36 | {friendlyTimeWithLineBreak(question.createdAt)} |
37 | {question.get('answered') ? friendlyTimeWithLineBreak(question.get('answeredAt')) : null} |
38 |
39 |
40 |
43 |
46 |
47 | |
48 |
49 | )
50 | })
51 |
52 | return (
53 |
54 |
55 |
56 | 问题 |
57 | 匿名 |
58 | 已回答 |
59 | 回答 |
60 | 回复 |
61 | 点赞 |
62 | 提问于 |
63 | 回答于 |
64 | 操作 |
65 |
66 |
67 | {rows}
68 |
69 | )
70 | }
71 | }
72 |
73 | const styles = {
74 | titleCell: {
75 | maxWidth: '200px'
76 | },
77 | answerCell: {
78 | maxWidth: '200px'
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/components/question/DraftsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class DraftsList extends React.Component {
8 | static propTypes = {
9 | questions: React.PropTypes.array.isRequired,
10 | openQuestionDetailsModal: React.PropTypes.func.isRequired,
11 | deleteQuestion: React.PropTypes.func.isRequired
12 | }
13 |
14 | render() {
15 | const { questions, openQuestionDetailsModal, deleteQuestion } = this.props
16 | const rows = questions.map(question => {
17 | const asker = question.get('asker')
18 |
19 | return (
20 |
21 |
22 |
23 | {asker ? ({asker.get('name')}) : "游客"}
24 |
25 | :
26 | {question.get('title')}
27 | |
28 | {question.get('anonymous') ? : null} |
29 |
30 | {truncate(question.get('draft'), 60)}
31 | |
32 | {friendlyTimeWithLineBreak(question.createdAt)} |
33 | {friendlyTimeWithLineBreak(question.get('draftedAt'))} |
34 |
35 |
36 |
39 |
42 |
43 | |
44 |
45 | )
46 | })
47 |
48 | return (
49 |
50 |
51 |
52 | 问题 |
53 | 匿名 |
54 | 草稿 |
55 | 提问于 |
56 | 最后编辑于 |
57 | 操作 |
58 |
59 |
60 | {rows}
61 |
62 | )
63 | }
64 | }
65 |
66 | const styles = {
67 | titleCell: {
68 | maxWidth: '200px'
69 | },
70 | answerCell: {
71 | maxWidth: '200px'
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/components/question/PendingQuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class PendingQuestionsList extends React.Component {
8 | static propTypes = {
9 | questions: React.PropTypes.array.isRequired,
10 | deleteQuestion: React.PropTypes.func.isRequired,
11 | openUpdateQuestionModal: React.PropTypes.func.isRequired,
12 | openQuestionDetailsModal: React.PropTypes.func.isRequired,
13 | }
14 |
15 | render() {
16 | const { questions, deleteQuestion, openUpdateQuestionModal, openQuestionDetailsModal } = this.props
17 | const rows = questions.map(question => {
18 | const asker = question.get('asker')
19 | return (
20 |
21 |
22 |
23 | {asker ? ({asker.get('name')}) : "游客"}
24 |
25 | :
26 | {question.get('title')}
27 | |
28 |
29 | {question.get('drafted') ? truncate(question.get('draft'), 60) : null}
30 | |
31 | {friendlyTimeWithLineBreak(question.createdAt)} |
32 |
33 |
34 |
37 |
40 |
43 |
44 | |
45 |
46 | )
47 | })
48 |
49 | return (
50 |
51 |
52 |
53 | 问题 |
54 | 草稿 |
55 | 提问于 |
56 | 操作 |
57 |
58 |
59 | {rows}
60 |
61 | )
62 | }
63 | }
64 |
65 | const styles = {
66 | titleCell: {
67 | maxWidth: '200px'
68 | },
69 | draftCell: {
70 | maxWidth: '200px'
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/components/question/QuestionCommentsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTime } from '../../filters'
5 |
6 | @Radium
7 | export default class QuestionCommentsList extends React.Component {
8 | static propTypes = {
9 | comments: React.PropTypes.array.isRequired,
10 | removeComment: React.PropTypes.func.isRequired
11 | }
12 |
13 | handleRemove = (comment) => {
14 | const { removeComment } = this.props
15 |
16 | if (!window.confirm('确认删除?')) {
17 | return
18 | }
19 |
20 | removeComment(comment)
21 | }
22 |
23 | render() {
24 | const { comments } = this.props
25 | const rows = comments.map((comment) => {
26 | const user = comment.get('user')
27 |
28 | return (
29 |
30 |
{friendlyTime(comment.createdAt)}
31 |
32 |
33 |
34 | {user.get('name')}
35 |
36 | :{comment.get('content')}
37 |
38 |
39 |
43 |
44 | )
45 | })
46 |
47 | return (
48 |
49 | {rows}
50 |
51 | )
52 | }
53 | }
54 |
55 | const styles = {
56 | wap: {
57 | padding: '5px 50px 5px 0',
58 | position: 'relative',
59 | ':hover': {
60 | backgroundColor: '#F2F2F2'
61 | }
62 | },
63 | timeWap: {
64 | fontSize: '12px',
65 | color: '#AAAAAA'
66 | },
67 | content: {},
68 | btnRemove: {
69 | position: 'absolute',
70 | right: '5px',
71 | top: '5px'
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/components/question/QuestionDetailsModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 | import { Link } from 'react-router'
5 | import BaseModal from "../public/BaseInfoModal"
6 | import QuestionCommentsList from './QuestionCommentsList'
7 | import Pagination from '../public/Pagination'
8 | import { friendlyTime, truncate } from '../../filters'
9 |
10 | const perPage = 10
11 |
12 | @Radium
13 | export default class QuestionDetailsModal extends React.Component {
14 | static propTypes = {
15 | question: React.PropTypes.object,
16 | modalIsOpen: React.PropTypes.bool.isRequired,
17 | closeModal: React.PropTypes.func.isRequired
18 | }
19 |
20 | state = {
21 | comments: [],
22 | commentsCurrentPage: 1,
23 | totalCommentsCount: this.props.question ? this.props.question.get('commentsCount') : 0
24 | }
25 |
26 | removeComment = (comment) => {
27 | comment.destroy().then(function () {
28 | this.setState({
29 | totalCommentsCount: this.state.totalCommentsCount - 1,
30 | comments: this.state.comments.filter(item => item.id !== comment.id)
31 | })
32 | }.bind(this))
33 | }
34 |
35 | componentWillReceiveProps(nextProps) {
36 | this.fetchData(1, nextProps.question)
37 | }
38 |
39 | componentDidMount() {
40 | this.fetchData(1, this.props.question)
41 | }
42 |
43 | fetchData = (page, question) => {
44 | if (!question) {
45 | this.setState({
46 | comments: []
47 | })
48 | return
49 | }
50 |
51 | this.setState({
52 | totalCommentsCount: question.get('commentsCount')
53 | })
54 |
55 | question.fetchComments(page, perPage).then(function (comments) {
56 | this.setState({comments, commentsCurrentPage: page})
57 | }.bind(this))
58 | }
59 |
60 | render() {
61 | const { question, modalIsOpen, closeModal } = this.props
62 | const asker = question ? question.get('asker') : null
63 | const user = question ? question.get('user') : null
64 |
65 | return (
66 |
70 |
71 |
72 | {friendlyTime(question.createdAt)}
73 |
74 |
75 |
76 | {asker ?
77 | {asker.get('name')} : "游客"}
78 |
79 | :{question.get('title')}
80 |
81 |
82 |
83 | {friendlyTime(question.get('answeredAt'))}
84 |
85 |
86 |
87 | {user.get('name')}
88 |
89 | :{truncate(question.get('answer'), 60)}
90 |
91 |
92 |
93 |
94 | {friendlyTime(question.get('draftedAt'))}
95 |
96 |
97 |
98 | {user.get('name')}
99 |
100 | :草稿
101 | {truncate(question.get('draft'), 60)}
102 |
103 |
104 |
105 | {question.get('likesCount')} 点赞,{this.state.totalCommentsCount} 评论
106 |
107 |
108 |
109 | {this.state.totalCommentsCount !== 0 ?
110 | (
111 |
114 | this.fetchData(page, question)}
118 | perPage={perPage}
119 | size="sm"/>
120 | ) : "无"}
121 |
122 |
123 |
124 | )
125 | }
126 | }
127 |
128 | const styles = {
129 | timeWap: {
130 | fontSize: '12px',
131 | color: '#AAAAAA'
132 | },
133 | hr: {
134 | margin: '10px 0',
135 | borderTop: '1px solid #E8E8E8'
136 | },
137 | draftFlag: {
138 | marginRight: '5px'
139 | }
140 | }
--------------------------------------------------------------------------------
/components/question/QuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class QuestionsList extends React.Component {
8 | static defaultProps = {
9 | showQuestionUser: true
10 | }
11 |
12 | static propTypes = {
13 | questions: React.PropTypes.array.isRequired,
14 | openUpdateQuestionModal: React.PropTypes.func.isRequired,
15 | openQuestionDetailsModal: React.PropTypes.func.isRequired,
16 | showQuestionUser: React.PropTypes.bool,
17 | deleteQuestion: React.PropTypes.func.isRequired
18 | }
19 |
20 | render() {
21 | const { questions, openUpdateQuestionModal, openQuestionDetailsModal, deleteQuestion } = this.props
22 | const rows = questions.map(question => {
23 | const asker = question.get('asker')
24 | const user = question.get('user')
25 |
26 | return (
27 |
28 |
29 |
30 | {asker ? ({asker.get('name')}) : "游客"}
31 |
32 | :
33 | {question.get('title')}
34 | |
35 | {question.get('anonymous') ? : null} |
36 | {this.props.showQuestionUser ?
37 | ({user.get('name')} | ) : null}
38 |
39 | {truncate(question.get('answer'), 60)}
40 | |
41 | {question.get('commentsCount')} |
42 | {question.get('likesCount')} |
43 | {friendlyTimeWithLineBreak(question.createdAt)} |
44 | {friendlyTimeWithLineBreak(question.get('answeredAt'))} |
45 |
46 |
47 |
50 |
54 |
57 |
58 | |
59 |
60 | )
61 | })
62 |
63 | return (
64 |
65 |
66 |
67 | 问题 |
68 | 匿名 |
69 | {this.props.showQuestionUser ? 被提问者 | : null}
70 | 回答 |
71 | 回复 |
72 | 点赞 |
73 | 提问于 |
74 | 回答于 |
75 | 操作 |
76 |
77 |
78 | {rows}
79 |
80 | )
81 | }
82 | }
83 |
84 | const styles = {
85 | titleCell: {
86 | maxWidth: '200px'
87 | },
88 | answerCell: {
89 | maxWidth: '200px'
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/components/question/UpdateQuestionModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 | import BaseModal from "../public/BaseFormModal"
5 |
6 | @Radium
7 | export default class UpdateQuestionModal extends React.Component {
8 | static propTypes = {
9 | question: React.PropTypes.object,
10 | updateQuestion: React.PropTypes.func.isRequired,
11 | modalIsOpen: React.PropTypes.bool.isRequired,
12 | closeModal: React.PropTypes.func.isRequired
13 | }
14 |
15 | state = {
16 | title: this.props.question ? this.props.question.get('title') : ""
17 | }
18 |
19 | componentWillReceiveProps = (nextProps) => {
20 | this.setState({
21 | title: nextProps.question ? nextProps.question.get('title') : ""
22 | })
23 | }
24 |
25 | handleTitleChange = (e) => {
26 | this.setState({title: e.target.value})
27 | }
28 |
29 | handleSubmit = () => {
30 | const { updateQuestion, closeModal } = this.props
31 |
32 | updateQuestion(this.props.question, this.state.title)
33 | closeModal()
34 | }
35 |
36 | render() {
37 | const { modalIsOpen, closeModal } = this.props
38 |
39 | return (
40 |
45 |
52 |
53 | )
54 | }
55 | }
56 |
57 | const styles = {}
--------------------------------------------------------------------------------
/components/reportedQuestions/ReportedQuestionsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { Link } from 'react-router'
4 | import { friendlyTimeWithLineBreak, truncate } from '../../filters'
5 |
6 | @Radium
7 | export default class ReportedQuestionsList extends React.Component {
8 | static propTypes = {
9 | reportedQuestions: React.PropTypes.array.isRequired,
10 | openUpdateQuestionModal: React.PropTypes.func.isRequired,
11 | openQuestionDetailsModal: React.PropTypes.func.isRequired,
12 | deleteQuestion: React.PropTypes.func.isRequired
13 | }
14 |
15 | render() {
16 | const { reportedQuestions, openQuestionDetailsModal, openUpdateQuestionModal, deleteQuestion } = this.props
17 | const rows = reportedQuestions.map(reportedQuestion => {
18 | const question = reportedQuestion.get('question')
19 | const reporter = reportedQuestion.get('reporter')
20 |
21 | return (
22 |
23 |
24 | {reporter
25 | ? {reporter.get('name')}
26 | : "游客"}
27 | |
28 |
29 |
30 | {question.get('asker') ?
31 |
32 | {question.get('user').get('name')}
33 | : "游客"}
34 |
35 | :
36 | {question.get('title')}
37 | |
38 |
39 |
40 |
41 | {question.get('user').get('name')}
42 |
43 |
44 | :
45 | {truncate(question.get('answer'), 60)}
46 | |
47 | {friendlyTimeWithLineBreak(reportedQuestion.createdAt)} |
48 |
49 |
50 |
53 |
56 |
59 |
60 | |
61 |
62 | )
63 | })
64 |
65 | return (
66 |
67 |
68 |
69 | 举报者 |
70 | 问题 |
71 | 回答 |
72 | 举报于 |
73 | 操作 |
74 |
75 |
76 | {rows}
77 |
78 | )
79 | }
80 | }
81 |
82 | const styles = {
83 | titleCell: {
84 | maxWidth: '200px'
85 | },
86 | answerCell: {
87 | maxWidth: '200px'
88 | }
89 | }
--------------------------------------------------------------------------------
/components/users/UserModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import Modal from "react-modal"
4 | import BaseModal from "../public/BaseFormModal"
5 |
6 | @Radium
7 | export default class UserModal extends React.Component {
8 | static propTypes = {
9 | user: React.PropTypes.object,
10 | modalIsOpen: React.PropTypes.bool.isRequired,
11 | updateUser: React.PropTypes.func.isRequired,
12 | closeModal: React.PropTypes.func.isRequired
13 | }
14 |
15 | state = {
16 | name: "",
17 | title: "",
18 | achievements: "",
19 | desc: "",
20 | mobilePhoneNumber: "",
21 | tags: ""
22 | }
23 |
24 | componentWillReceiveProps = (nextProps) => {
25 | if (nextProps.user && nextProps.modalIsOpen) {
26 | this.setState({
27 | name: nextProps.user.get('name'),
28 | title: nextProps.user.get('title'),
29 | achievements: nextProps.user.get('achievements'),
30 | desc: nextProps.user.get('desc'),
31 | mobilePhoneNumber: nextProps.user.get('mobilePhoneNumber'),
32 | tags: nextProps.user.get('tags').join(', ')
33 | })
34 | }
35 | }
36 |
37 | handleNameChange = (e) => {
38 | this.setState({name: e.target.value})
39 | }
40 |
41 | handleTitleChange = (e) => {
42 | this.setState({title: e.target.value})
43 | }
44 |
45 | handleDescChange = (e) => {
46 | this.setState({desc: e.target.value})
47 | }
48 |
49 | handleAchievementsChange = (e) => {
50 | this.setState({achievements: e.target.value})
51 | }
52 |
53 | handleMobilePhoneNumberChange = (e) => {
54 | this.setState({mobilePhoneNumber: e.target.value})
55 | }
56 |
57 | handleTagsChange = (e) => {
58 | this.setState({tags: e.target.value})
59 | }
60 |
61 | handleSubmit = () => {
62 | const { user, updateUser, closeModal } = this.props
63 | const tags = this.state.tags.replace(/ /g, '').split(',')
64 |
65 | closeModal()
66 | updateUser(user, this.state.name, this.state.title, this.state.achievements, this.state.desc,
67 | this.state.mobilePhoneNumber, tags)
68 | }
69 |
70 | render() {
71 | const { modalIsOpen, closeModal } = this.props
72 |
73 | return (
74 |
79 |
112 |
113 | )
114 | }
115 | }
116 |
117 | const styles = {}
118 |
--------------------------------------------------------------------------------
/components/users/UserPageHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import { friendlyTime } from '../../filters'
4 |
5 | @Radium
6 | export default class UserPageHeader extends React.Component {
7 | static propTypes = {
8 | user: React.PropTypes.object.isRequired
9 | }
10 |
11 | render() {
12 | const { user } = this.props
13 | const notificationsReadStatus = []
14 | const notificationsSwitchStatus = []
15 |
16 | if (user.get('unreadAskQuestionNotificationsCount')) {
17 | notificationsReadStatus.push(`${user.get('unreadAskQuestionNotificationsCount')}条未读提问通知`)
18 | }
19 | if (user.get('unreadOtherNotificationsCount')) {
20 | notificationsReadStatus.push(`${user.get('unreadOtherNotificationsCount')}条未读其他通知`)
21 | }
22 |
23 | if (!user.get('allowRemoteNotification')) {
24 | notificationsSwitchStatus.push('不接收通知')
25 | }
26 | if (!user.get('allowAskQuestionInnerNotification')) {
27 | notificationsSwitchStatus.push('不显示“被提问”应用内红点提示')
28 | }
29 | if (!user.get('allowAnswerQuestionInnerNotification')) {
30 | notificationsSwitchStatus.push('不显示“提问被回答”应用内红点提示')
31 | }
32 | if (!user.get('allowCommentQuestionInnerNotification')) {
33 | notificationsSwitchStatus.push('不显示“被回复”应用内红点提示')
34 | }
35 | if (!user.get('allowLikeQuestionInnerNotification')) {
36 | notificationsSwitchStatus.push('不显示“被赞”应用内红点提示')
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |

45 |
46 |
47 |
48 | {user.get('name')}
49 |
50 | {user.get('authed')
51 | ? 已认证
53 | : 未认证}
55 |
56 | {user.get('opened')
57 | ? 已开通
59 | : 未开通}
61 |
62 | {user.get('processingQARequest')
63 | ? 申请开通
65 | : null}
66 |
67 | {user.get('title')}
68 |
69 |
70 |
71 |
72 |
注册于:{friendlyTime(user.createdAt)}
73 |
最后活跃于
74 | :{user.get('lastActiveAt') ? friendlyTime(user.get('lastActiveAt')) : "暂无数据"}
75 |
手机:{user.get('mobilePhoneNumber')}
76 |
标签:{user.get('tags') ? user.get('tags').join(', ') : null}
77 |
获赞:{user.get('likesCount')}
78 |
成就:{user.get('achievements')}
79 |
简介:{user.get('desc')}
80 |
81 | 通知状态:
82 | {notificationsReadStatus.length > 0 ? notificationsReadStatus.join(', ') : "无未读通知"}
83 |
84 |
85 | 通知开关:
86 | {notificationsSwitchStatus.length > 0 ? notificationsSwitchStatus.join(', ') : "全部打开"}
87 |
88 |
89 | )
90 | }
91 | }
92 |
93 | const styles = {
94 | header: {
95 | marginBottom: '15px'
96 | },
97 | name: {
98 | marginTop: '4px'
99 | },
100 | statusLabel: {
101 | marginLeft: '10px',
102 | fontSize: '12px'
103 | },
104 | desc: {
105 | marginBottom: '15px'
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/components/users/UsersList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Radium from 'radium'
3 | import moment from 'moment'
4 | import { Link } from 'react-router'
5 | import { truncate, friendlyTimeWithLineBreak } from '../../filters'
6 |
7 | @Radium
8 | export default class UsersList extends React.Component {
9 |
10 | handleOpenUserQAPage = (user) => {
11 | const { openUserQAPage } = this.props
12 |
13 | if (window.confirm('确认开通?')) {
14 | openUserQAPage(user)
15 | }
16 | }
17 |
18 | handleCloseUserQAPage = (user) => {
19 | const { closeUserQAPage } = this.props
20 |
21 | if (window.confirm('确认关闭?')) {
22 | closeUserQAPage(user)
23 | }
24 | }
25 |
26 | render() {
27 | const { users, openModal } = this.props
28 | const userNodes = users.map(user => {
29 | return (
30 |
31 |
32 |
34 | |
35 |
36 | {user.get('name')}
37 | |
38 | {user.get('title')} |
39 | {user.get('authed') ? null : ()} |
40 | {user.get('opened') ? null : ()} |
41 | {user.get('processingQARequest') ? (
42 | 待处理) : null} |
43 |
44 |
45 |
46 |
47 | |
48 | {user.get('answeredQuestionsCount')} |
49 | {user.get('tags').join(", ")} |
50 | {truncate(user.get('achievements'), 20)} |
51 | {truncate(user.get('desc'), 20)} |
52 | {friendlyTimeWithLineBreak(user.createdAt)} |
53 |
54 |
55 |
57 |
58 |
59 |
62 |
63 |
64 |
65 |
68 |
69 |
70 | |
71 |
72 | )
73 | })
74 |
75 | return (
76 |
77 |
78 |
79 | |
80 | 用户 |
81 | 身份 |
82 | 认证 |
83 | 开通 Q&A |
84 | Q&A 申请 |
85 | 手机 |
86 | 问答 |
87 | 标签 |
88 | 成就 |
89 | 简介 |
90 | 注册于 |
91 | 操作 |
92 |
93 |
94 | {userNodes}
95 |
96 | )
97 | }
98 | }
99 |
100 | const styles = {
101 | name: {
102 | fontWeight: 'bold'
103 | },
104 | titleCell: {
105 | maxWidth: '120px'
106 | },
107 | tagsCell: {
108 | maxWidth: '140px'
109 | },
110 | achievementsCell: {
111 | maxWidth: '140px'
112 | },
113 | descCell: {
114 | maxWidth: '140px'
115 | }
116 | }
--------------------------------------------------------------------------------
/containers/AnswersPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import Radium from 'radium'
5 | import QuestionsList from '../components/question/QuestionsList'
6 | import { fetchAnswers, updateQuestion, deleteQuestion as _deleteQuestion } from '../actions/question'
7 | import Pagination from '../components/public/Pagination'
8 | import QuestionDetailsModal from '../components/question/QuestionDetailsModal'
9 | import UpdateQuestionModal from '../components/question/UpdateQuestionModal'
10 |
11 | const perPage = 15
12 |
13 | @Radium
14 | export default class AnswersPage extends React.Component {
15 | state = {
16 | updateQuestionModalIsOpen: false,
17 | questionDetailsModalIsOpen: false,
18 |
19 | questionForUpdateModal: null,
20 | questionForDetailsModal: null
21 | }
22 |
23 | componentDidMount() {
24 | const { dispatch } = this.props
25 |
26 | dispatch(fetchAnswers(1, perPage))
27 | }
28 |
29 | deleteQuestion = (question) => {
30 | const { dispatch } = this.props
31 |
32 | if (!window.confirm('确认删除此问题')) {
33 | return
34 | }
35 |
36 | dispatch(_deleteQuestion(question))
37 | }
38 |
39 | openUpdateQuestionModal = (question) => {
40 | this.setState({
41 | questionForUpdateModal: question,
42 | updateQuestionModalIsOpen: true
43 | })
44 | }
45 |
46 | closeUpdateQuestionModal = () => {
47 | this.setState({
48 | questionForUpdateModal: null,
49 | updateQuestionModalIsOpen: false
50 | })
51 | }
52 |
53 | openQuestionDetailsModal = (question) => {
54 | this.setState({
55 | questionForDetailsModal: question,
56 | questionDetailsModalIsOpen: true
57 | })
58 | }
59 |
60 | closeQuestionDetailsModal = () => {
61 | this.setState({
62 | questionForDetailsModal: null,
63 | questionDetailsModalIsOpen: false
64 | })
65 | }
66 |
67 | render() {
68 | const { dispatch } = this.props
69 |
70 | return (
71 |
72 |
回答
73 |
74 |
79 |
80 | fetchAnswers(page, perPage), dispatch)}
84 | perPage={perPage}/>
85 |
86 |
90 |
91 |
94 |
95 | )
96 | }
97 | }
98 |
99 | const mapStateToProps = (state) => {
100 | return {
101 | currentPage: state.answers.currentPage,
102 | totalCount: state.answers.totalCount,
103 | answers: state.answers.answers
104 | }
105 | }
106 |
107 | const styles = {}
108 |
109 | export default connect(mapStateToProps)(AnswersPage)
110 |
--------------------------------------------------------------------------------
/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { connect } from 'react-redux'
3 | import { Router, Route, IndexRoute, Link, browserHistory } from 'react-router'
4 | import { logOut } from '../actions/account'
5 | import ActiveNavItem from '../components/public/ActiveNavItem'
6 |
7 | class App extends Component {
8 | handleLogOut = () => {
9 | const { dispatch } = this.props
10 |
11 | dispatch(logOut())
12 | }
13 |
14 | render() {
15 | const { path, authed } = this.props
16 |
17 | return (
18 |
19 |
44 |
45 |
46 | {this.props.children}
47 |
48 |
49 | )
50 | }
51 | }
52 |
53 | function mapStateToProps(state) {
54 | return {
55 | authed: state.account.authed,
56 | path: state.routing.locationBeforeTransitions.pathname
57 | }
58 | }
59 |
60 | export default connect(mapStateToProps)(App)
61 |
--------------------------------------------------------------------------------
/containers/AuthPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { browserHistory } from 'react-router'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import Radium from 'radium'
6 | import * as auth from '../auth'
7 | import { logIn } from '../actions/account'
8 |
9 | @Radium
10 | export default class AuthPage extends React.Component {
11 | state = {
12 | password: ""
13 | }
14 |
15 | componentDidMount() {
16 | this.refs.password.focus()
17 | }
18 |
19 | handlePasswordChange = (e) => {
20 | this.setState({password: e.target.value.trim()})
21 | }
22 |
23 | handleSubmit = (e) => {
24 | const { dispatch } = this.props
25 |
26 | e.preventDefault()
27 | dispatch(logIn(this.state.password))
28 | this.setState({password: ""})
29 | }
30 |
31 | render() {
32 | const { dispatch } = this.props
33 |
34 | return (
35 |
36 |
37 |
谁?
38 |
39 |
46 |
47 |
48 | )
49 | }
50 | }
51 |
52 | const mapStateToProps = (state) => {
53 | return {}
54 | }
55 |
56 | const styles = {}
57 |
58 | export default connect(mapStateToProps)(AuthPage)
59 |
--------------------------------------------------------------------------------
/containers/BroadcastSystemNotificationsPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import { fetchBroadcastSystemNotifications, pushBroadcastSystemNotification } from '../actions/broadcastSystemNotifications'
5 | import BroadcastSystemNotificationsList from '../components/notification/BroadcastSystemNotificationsList'
6 |
7 | export default class BroadcastSystemNotificationsPage extends React.Component {
8 | state = {
9 | content: ''
10 | }
11 |
12 | componentDidMount() {
13 | const { dispatch } = this.props
14 |
15 | dispatch(fetchBroadcastSystemNotifications())
16 | }
17 |
18 | handleContentChange = (e) => {
19 | this.setState({content: e.target.value.trim()})
20 | }
21 |
22 | handleSubmit = (e) => {
23 | const { dispatch } = this.props
24 |
25 | e.preventDefault()
26 |
27 | if (this.state.content === '') {
28 | window.alert('请输入内容')
29 | return
30 | }
31 |
32 | dispatch(pushBroadcastSystemNotification(this.state.content))
33 | this.setState({content: ""})
34 | }
35 |
36 | render() {
37 | return (
38 |
39 |
40 |
推送系统通知
41 |
42 |
50 |
51 |
推送记录
52 |
53 |
54 |
55 |
56 | )
57 | }
58 | }
59 |
60 | const mapStateToProps = (state) => {
61 | return {
62 | notifications: state.broadcastSystemNotifications
63 | }
64 | }
65 |
66 | export default connect(mapStateToProps)(BroadcastSystemNotificationsPage)
67 |
--------------------------------------------------------------------------------
/containers/DashboardPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import { Link } from 'react-router'
5 | import { fetchDashboardData } from '../actions/dashboard'
6 |
7 | export default class DashboardPage extends React.Component {
8 | componentDidMount() {
9 | const { dispatch } = this.props
10 |
11 | dispatch(fetchDashboardData())
12 | }
13 |
14 | render() {
15 | const { dispatch, usersCount, questionsCount, answersCount, reportsCount } = this.props
16 |
17 | return (
18 |
19 |
Dashboard
20 |
21 |
22 |
23 |
24 |
25 |
26 | 今日注册用户 |
27 | {usersCount} |
28 |
29 | 详情
30 | |
31 |
32 |
33 | 今日提问 |
34 | {questionsCount} |
35 |
36 | 详情
37 | |
38 |
39 |
40 | 今日回答 |
41 | {answersCount} |
42 |
43 | 详情
44 | |
45 |
46 |
47 | 今日举报 |
48 | {reportsCount} |
49 |
50 | 详情
51 | |
52 |
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 | }
61 |
62 | const mapStateToProps = (state) => {
63 | return {
64 | usersCount: state.dashboard.usersCount,
65 | questionsCount: state.dashboard.questionsCount,
66 | answersCount: state.dashboard.answersCount,
67 | reportsCount: state.dashboard.reportsCount
68 | }
69 | }
70 |
71 | export default connect(mapStateToProps)(DashboardPage)
72 |
--------------------------------------------------------------------------------
/containers/DevTools.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createDevTools } from 'redux-devtools'
3 | import LogMonitor from 'redux-devtools-log-monitor'
4 | import DockMonitor from 'redux-devtools-dock-monitor'
5 |
6 | export default createDevTools(
7 |
10 |
11 |
12 | )
13 |
--------------------------------------------------------------------------------
/containers/HomeRecommendationsPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import Radium from 'radium'
5 | import { fetchHomeRecommendations, removeHomeRecommendation,
6 | updateHomeRecommendationUserBackgroundImage, addHomeRecommendation, topHomeRecommendation } from '../actions/homeRecommendations'
7 | import Pagination from '../components/public/Pagination'
8 | import HomeRecommendationsList from '../components/homeRecommendations/HomeRecommendationsList'
9 | import AddHomeRecommendationModal from '../components/homeRecommendations/AddHomeRecommendationModal'
10 |
11 | const perPage = 15
12 |
13 | @Radium
14 | export default class HomeRecommendationsPage extends React.Component {
15 | state = {
16 | modalIsOpen: false
17 | }
18 |
19 | openModal = () => {
20 | this.setState({modalIsOpen: true})
21 | }
22 |
23 | closeModal = () => {
24 | this.setState({modalIsOpen: false})
25 | }
26 |
27 | componentDidMount() {
28 | const { dispatch } = this.props
29 |
30 | dispatch(fetchHomeRecommendations(1, perPage))
31 | }
32 |
33 | selectImageFile = (homeRecommendation) => {
34 | this.targetHomeRecommendation = homeRecommendation
35 |
36 | if (this.fileInput !== null) {
37 | this.fileInput.click();
38 | }
39 | }
40 |
41 | handleInputChange = (e) => {
42 | const { dispatch } = this.props
43 |
44 | dispatch(updateHomeRecommendationUserBackgroundImage(this.targetHomeRecommendation, e.target.files[0]))
45 | }
46 |
47 | render() {
48 | const { dispatch, recommendations } = this.props
49 |
50 | return (
51 |
52 |
首页推荐管理
53 |
54 |
55 |
58 |
59 |
60 |
61 |
66 |
67 |
68 |
this.fileInput = ref}
69 | onChange={this.handleInputChange}/>
70 |
71 |
75 |
76 |
fetchHomeRecommendations(page, perPage), dispatch)}
80 | perPage={perPage}/>
81 |
82 | )
83 | }
84 | }
85 |
86 | const mapStateToProps = (state) => {
87 | return {
88 | totalCount: state.homeRecommendations.totalCount,
89 | currentPage: state.homeRecommendations.currentPage,
90 | recommendations: state.homeRecommendations.recommendations[state.homeRecommendations.currentPage] || []
91 | }
92 | }
93 |
94 | const styles = {
95 | homeRecommendationList: {
96 | marginTop: '10px'
97 | }
98 | }
99 |
100 |
101 | export default connect(mapStateToProps)(HomeRecommendationsPage)
102 |
--------------------------------------------------------------------------------
/containers/InvitationCodesPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import Radium from 'radium'
5 | import { fetchInvitationCodes, markInvitationSended, generateInvitationCodes } from '../actions/invitationCodes'
6 | import Pagination from '../components/public/Pagination'
7 | import InvitationCodesList from '../components/invitationCodes/InvitationCodesList'
8 | import MarkInvitationCodeSendedModal from '../components/invitationCodes/MarkInvitationCodeSendedModal'
9 |
10 | const perPage = 15
11 |
12 | @Radium
13 | export default class InvitationCodesPage extends React.Component {
14 | state = {
15 | modalIsOpen: false,
16 | codeForModal: null
17 | }
18 |
19 | openModal = (code) => {
20 | this.setState({modalIsOpen: true, codeForModal: code})
21 | }
22 |
23 | closeModal = () => {
24 | this.setState({modalIsOpen: false})
25 | }
26 |
27 | componentDidMount() {
28 | const { dispatch } = this.props
29 |
30 | dispatch(fetchInvitationCodes(this.props.currentPage, perPage))
31 | }
32 |
33 | handleGenerateCodes = () => {
34 | const { dispatch } = this.props
35 |
36 | dispatch(generateInvitationCodes())
37 | }
38 |
39 | render() {
40 | const { dispatch } = this.props
41 |
42 | return (
43 |
44 |
邀请码管理
45 |
46 |
47 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
64 |
65 |
fetchInvitationCodes(page, perPage), dispatch)}
69 | perPage={perPage}/>
70 |
71 | )
72 | }
73 | }
74 |
75 | function mapStateToProps(state) {
76 | return {
77 | totalCount: state.invitationCodes.totalCount,
78 | currentPage: state.invitationCodes.currentPage,
79 | codes: state.invitationCodes.codes[state.invitationCodes.currentPage] || []
80 | }
81 | }
82 |
83 | const styles = {
84 | invitationCodeList: {
85 | marginTop: '10px'
86 | }
87 | }
88 |
89 | export default connect(mapStateToProps)(InvitationCodesPage)
90 |
--------------------------------------------------------------------------------
/containers/PendingAnonymousQuestionsPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import { fetchPendingAnonymousQuestions, updateQuestion, deleteQuestion as _deleteQuestion } from '../actions/question'
5 | import Pagination from '../components/public/Pagination'
6 | import QuestionsList from '../components/question/QuestionsList'
7 | import QuestionDetailsModal from '../components/question/QuestionDetailsModal'
8 | import UpdateQuestionModal from '../components/question/UpdateQuestionModal'
9 |
10 | const perPage = 15
11 |
12 | export default class PendingAnonymousQuestionsPage extends React.Component {
13 | state = {
14 | updateQuestionModalIsOpen: false,
15 | questionDetailsModalIsOpen: false,
16 |
17 | questionForUpdateModal: null,
18 | questionForDetailsModal: null
19 | }
20 |
21 | componentDidMount() {
22 | const { dispatch } = this.props
23 |
24 | dispatch(fetchPendingAnonymousQuestions(1, perPage))
25 | }
26 |
27 | deleteQuestion = (question) => {
28 | const { dispatch } = this.props
29 |
30 | if (!window.confirm('确认删除此问题')) {
31 | return
32 | }
33 |
34 | dispatch(_deleteQuestion(question))
35 | }
36 |
37 | openUpdateQuestionModal = (question) => {
38 | this.setState({
39 | questionForUpdateModal: question,
40 | updateQuestionModalIsOpen: true
41 | })
42 | }
43 |
44 | closeUpdateQuestionModal = () => {
45 | this.setState({
46 | questionForUpdateModal: null,
47 | updateQuestionModalIsOpen: false
48 | })
49 | }
50 |
51 | openQuestionDetailsModal = (question) => {
52 | this.setState({
53 | questionForDetailsModal: question,
54 | questionDetailsModalIsOpen: true
55 | })
56 | }
57 |
58 | closeQuestionDetailsModal = () => {
59 | this.setState({
60 | questionForDetailsModal: null,
61 | questionDetailsModalIsOpen: false
62 | })
63 | }
64 |
65 | render() {
66 | const { dispatch, questions } = this.props
67 |
68 | return (
69 |
70 |
待回答的匿名提问
71 |
72 |
77 |
78 |
82 |
83 |
86 |
87 | fetchPendingAnonymousQuestions(page, perPage), dispatch)}
91 | perPage={perPage}/>
92 |
93 | )
94 | }
95 | }
96 |
97 | const mapStateToProps = (state) => {
98 | return {
99 | totalCount: state.pendingAnonymousQuestions.totalCount,
100 | currentPage: state.pendingAnonymousQuestions.currentPage,
101 | questions: state.pendingAnonymousQuestions.questions
102 | }
103 | }
104 |
105 | export default connect(mapStateToProps)(PendingAnonymousQuestionsPage)
106 |
--------------------------------------------------------------------------------
/containers/QuestionsPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import { fetchQuestions, updateQuestion, deleteQuestion as _deleteQuestion } from '../actions/question'
5 | import Pagination from '../components/public/Pagination'
6 | import QuestionsList from '../components/question/QuestionsList'
7 | import QuestionDetailsModal from '../components/question/QuestionDetailsModal'
8 | import UpdateQuestionModal from '../components/question/UpdateQuestionModal'
9 |
10 | const perPage = 15
11 |
12 | export default class QuestionsPage extends React.Component {
13 | state = {
14 | updateQuestionModalIsOpen: false,
15 | questionDetailsModalIsOpen: false,
16 |
17 | questionForUpdateModal: null,
18 | questionForDetailsModal: null
19 | }
20 |
21 | componentDidMount() {
22 | const { dispatch } = this.props
23 |
24 | dispatch(fetchQuestions(1, perPage))
25 | }
26 |
27 | deleteQuestion = (question) => {
28 | const { dispatch } = this.props
29 |
30 | if (!window.confirm('确认删除此问题')) {
31 | return
32 | }
33 |
34 | dispatch(_deleteQuestion(question))
35 | }
36 |
37 | openUpdateQuestionModal = (question) => {
38 | this.setState({
39 | questionForUpdateModal: question,
40 | updateQuestionModalIsOpen: true
41 | })
42 | }
43 |
44 | closeUpdateQuestionModal = () => {
45 | this.setState({
46 | questionForUpdateModal: null,
47 | updateQuestionModalIsOpen: false
48 | })
49 | }
50 |
51 | openQuestionDetailsModal = (question) => {
52 | this.setState({
53 | questionForDetailsModal: question,
54 | questionDetailsModalIsOpen: true
55 | })
56 | }
57 |
58 | closeQuestionDetailsModal = () => {
59 | this.setState({
60 | questionForDetailsModal: null,
61 | questionDetailsModalIsOpen: false
62 | })
63 | }
64 |
65 | render() {
66 | const { dispatch, questions } = this.props
67 |
68 | return (
69 |
70 |
提问
71 |
72 |
77 |
78 | fetchQuestions(page, perPage), dispatch)}
82 | perPage={perPage}/>
83 |
84 |
88 |
89 |
92 |
93 | )
94 | }
95 | }
96 |
97 | const mapStateToProps = (state) => {
98 | return {
99 | totalCount: state.questions.totalCount,
100 | currentPage: state.questions.currentPage,
101 | questions: state.questions.questions
102 | }
103 | }
104 |
105 | export default connect(mapStateToProps)(QuestionsPage)
106 |
--------------------------------------------------------------------------------
/containers/ReportsPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import { updateQuestion, deleteQuestion as _deleteQuestion } from '../actions/question'
5 | import { fetchReportedQuestions } from '../actions/reportedQuestions'
6 | import Pagination from '../components/public/Pagination'
7 | import ReportedQuestionsList from '../components/reportedQuestions/ReportedQuestionsList'
8 | import QuestionDetailsModal from '../components/question/QuestionDetailsModal'
9 | import UpdateQuestionModal from '../components/question/UpdateQuestionModal'
10 |
11 | const perPage = 15
12 |
13 | export default class ReportsPage extends React.Component {
14 | state = {
15 | updateQuestionModalIsOpen: false,
16 | questionDetailsModalIsOpen: false,
17 |
18 | questionForUpdateModal: null,
19 | questionForDetailsModal: null
20 | }
21 |
22 | componentDidMount() {
23 | const { dispatch } = this.props
24 |
25 | dispatch(fetchReportedQuestions(1, perPage))
26 | }
27 |
28 | deleteQuestion = (question) => {
29 | const { dispatch } = this.props
30 |
31 | if (!window.confirm('确认删除此问题')) {
32 | return
33 | }
34 |
35 | dispatch(_deleteQuestion(question))
36 | }
37 |
38 | openUpdateQuestionModal = (question) => {
39 | this.setState({
40 | questionForUpdateModal: question,
41 | updateQuestionModalIsOpen: true
42 | })
43 | }
44 |
45 | closeUpdateQuestionModal = () => {
46 | this.setState({
47 | questionForUpdateModal: null,
48 | updateQuestionModalIsOpen: false
49 | })
50 | }
51 |
52 | openQuestionDetailsModal = (question) => {
53 | this.setState({
54 | questionForDetailsModal: question,
55 | questionDetailsModalIsOpen: true
56 | })
57 | }
58 |
59 | closeQuestionDetailsModal = () => {
60 | this.setState({
61 | questionForDetailsModal: null,
62 | questionDetailsModalIsOpen: false
63 | })
64 | }
65 |
66 | render() {
67 | const { dispatch, reportedQuestions } = this.props
68 |
69 | return (
70 |
71 |
问题举报
72 |
73 |
78 |
79 | fetchReportedQuestions(page, perPage), dispatch)}
83 | perPage={perPage}/>
84 |
85 |
89 |
90 |
93 |
94 | )
95 | }
96 | }
97 |
98 | const mapStateToProps = (state) => {
99 | return {
100 | totalCount: state.reportedQuestions.totalCount,
101 | currentPage: state.reportedQuestions.currentPage,
102 | reportedQuestions: state.reportedQuestions.reportedQuestions
103 | }
104 | }
105 |
106 | export default connect(mapStateToProps)(ReportsPage)
107 |
--------------------------------------------------------------------------------
/containers/Root.dev.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { Provider } from 'react-redux'
3 | import routes from '../routes'
4 | import DevTools from './DevTools'
5 | import { Router } from 'react-router'
6 |
7 | export default class Root extends React.Component {
8 | render() {
9 | const { store, history } = this.props
10 | return (
11 |
12 |
13 |
14 | {/**/}
15 |
16 |
17 | )
18 | }
19 | }
20 |
21 | Root.propTypes = {
22 | store: PropTypes.object.isRequired,
23 | history: PropTypes.object.isRequired
24 | }
25 |
--------------------------------------------------------------------------------
/containers/Root.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./Root.prod')
3 | } else {
4 | module.exports = require('./Root.dev')
5 | }
6 |
--------------------------------------------------------------------------------
/containers/Root.prod.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import { Provider } from 'react-redux'
3 | import { Router } from 'react-router'
4 | import routes from '../routes'
5 |
6 | export default class Root extends React.Component {
7 | render() {
8 | const { store, history } = this.props
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | Root.propTypes = {
18 | store: PropTypes.object.isRequired,
19 | history: PropTypes.object.isRequired
20 | }
--------------------------------------------------------------------------------
/containers/UserPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 | import update from 'react-addons-update'
5 | import Radium from 'radium'
6 | import AV from 'avoscloud-sdk'
7 | import { User } from '../models'
8 | import UserPageHeader from '../components/users/UserPageHeader'
9 | import UserTabs from '../components/users/UserTabs'
10 | import AskQuestionModal from '../components/question/AskQuestionModal'
11 |
12 | @Radium
13 | export default class UserPage extends React.Component {
14 | state = {
15 | user: null,
16 | askQuestionModalIsOpen: false
17 | }
18 |
19 | componentWillReceiveProps(nextProps) {
20 | if (this.props.params.id !== nextProps.params.id) {
21 | this.fetchData(nextProps.params.id)
22 | }
23 | }
24 |
25 | componentDidMount() {
26 | this.fetchData(this.props.params.id)
27 | }
28 |
29 |
30 | askQuestion = (title) => {
31 | User.askAnonymousQuestion(this.state.user, title).then(function (question) {
32 | this.setState({
33 | user: update(this.state.user, {
34 | attributes: {pendingQuestionsCount: {$apply: count => count + 1}}
35 | }),
36 | pendingQuestions: update(this.state.pendingQuestions, {$unshift: [question]})
37 | })
38 | }.bind(this))
39 | }
40 |
41 | fetchData = (id) => {
42 | const user = AV.Object.createWithoutData('_User', id)
43 |
44 | user.fetch().then(function (user) {
45 | this.setState({user: user})
46 | }.bind(this))
47 | }
48 |
49 | openAskQuestionModal = () => {
50 | this.setState({askQuestionModalIsOpen: true})
51 | }
52 |
53 | closeAskQuestionModal = () => {
54 | this.setState({askQuestionModalIsOpen: false})
55 | }
56 |
57 | render() {
58 | const { user } = this.state
59 |
60 | return (
61 |
62 |
63 |
64 | {user ? : null}
65 |
66 |
67 |
68 |
69 | {/*
70 |
73 | {user && !user.get('opened') ?
74 | : null}
77 | */}
78 | {user && user.get('opened') ?
79 | : null}
82 |
83 |
84 |
85 |
86 |
87 | {user ? : null}
88 |
89 |
90 |
94 |
95 | )
96 | }
97 | }
98 |
99 | //const mapStateToProps = (state) => {
100 | // return {}
101 | //}
102 |
103 | const styles = {
104 | header: {
105 | marginBottom: '15px'
106 | },
107 | name: {
108 | marginTop: '4px'
109 | },
110 | statusLabel: {
111 | marginLeft: '10px',
112 | fontSize: '12px'
113 | },
114 | desc: {
115 | marginBottom: '15px'
116 | },
117 | tabs: {
118 | marginTop: '10px'
119 | }
120 | }
121 |
122 | export default UserPage
123 | //export default connect(mapStateToProps)(UserPage)
124 |
--------------------------------------------------------------------------------
/containers/UsersPage.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Radium from 'radium'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import Pagination from '../components/public/Pagination'
6 | import UsersList from '../components/users/UsersList'
7 | import UserModal from '../components/users/UserModal'
8 | import { fetchUsers, openUserQAPage, closeUserQAPage, updateUser, searchUsers } from '../actions/users'
9 |
10 | const perPage = 15
11 |
12 | @Radium
13 | export default class UsersPage extends Component {
14 | state = {
15 | modalIsOpen: false,
16 | userForModal: null,
17 | searchKeyword: ""
18 | }
19 |
20 | openModal = (user) => {
21 | this.setState({modalIsOpen: true, userForModal: user})
22 | }
23 |
24 | closeModal = () => {
25 | this.setState({modalIsOpen: false})
26 | }
27 |
28 | componentDidMount() {
29 | const { dispatch } = this.props
30 |
31 | dispatch(fetchUsers(1, perPage))
32 | }
33 |
34 | handleKeywordChange = (e) => {
35 | const { dispatch } = this.props
36 | const keyword = e.target.value.trim()
37 |
38 | this.setState({searchKeyword: keyword})
39 |
40 | if (keyword === "") {
41 | dispatch(fetchUsers(1, perPage))
42 | }
43 | }
44 |
45 | handleSearch = () => {
46 | const { dispatch } = this.props
47 |
48 | if (this.state.searchKeyword === "") {
49 | dispatch(fetchUsers(1, perPage))
50 | } else {
51 | dispatch(searchUsers(this.state.searchKeyword))
52 | }
53 | }
54 |
55 | render() {
56 | const { dispatch } = this.props
57 |
58 | return (
59 |
60 |
用户管理
61 |
62 |
63 |
65 |
66 |
69 |
70 |
71 |
72 |
76 |
77 |
81 |
82 | fetchUsers(page, perPage), dispatch)}
86 | perPage={perPage}/>
87 |
88 | )
89 | }
90 | }
91 |
92 | const mapStateToProps = (state) => {
93 | return {
94 | users: state.users.users,
95 | currentPage: state.users.currentPage,
96 | totalCount: state.users.totalCount
97 | }
98 | }
99 |
100 | const styles = {
101 | searchWap: {
102 | maxWidth: "500px",
103 | marginBottom: "10px"
104 | }
105 | }
106 |
107 | export default connect(mapStateToProps)(UsersPage)
108 |
--------------------------------------------------------------------------------
/containers/VisitorQuestionsReviewPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import { bindActionCreators } from 'redux'
4 |
5 | export default class VisitorQuestionsReviewPage extends React.Component {
6 | componentDidMount() {
7 | const { dispatch } = this.props
8 |
9 | }
10 |
11 | render() {
12 | const { dispatch } = this.props
13 |
14 | return (
15 |
16 |
匿名提问审核
17 |
18 | )
19 | }
20 | }
21 |
22 | const mapStateToProps = (state) => {
23 | return {
24 | questions: []
25 | }
26 | }
27 |
28 | export default connect(mapStateToProps)(VisitorQuestionsReviewPage)
29 |
--------------------------------------------------------------------------------
/fabfile.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from fabric.api import run, env, cd
3 |
4 | def deploy():
5 | env.host_string = "root@12.34.56.78"
6 | with cd('/var/www/react-redux-example'):
7 | run('git reset --hard HEAD')
8 | run('git pull')
9 | run('npm install')
10 | run('npm run build')
11 | run('npm run upload')
12 |
--------------------------------------------------------------------------------
/filters.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import moment from 'moment'
3 |
4 | export function friendlyTime(time) {
5 | if (!time) {
6 | return ""
7 | }
8 |
9 | return (
10 | {moment(time).format('YYYY-MM-DD HH:mm:ss')}
11 | )
12 | }
13 |
14 | export function friendlyTimeWithLineBreak(time) {
15 | if (!time) {
16 | return ""
17 | }
18 |
19 | return (
20 |
21 | {moment(time).format('YYYY-MM-DD')}
22 |
23 | {moment(time).format('HH:mm:ss')}
24 |
25 | )
26 | }
27 |
28 | export function truncate(content, maxLength) {
29 | return content && content.length > maxLength ? content.substring(0, maxLength) + "..." : content
30 | }
31 |
--------------------------------------------------------------------------------
/fis-conf.js:
--------------------------------------------------------------------------------
1 | fis.set('project.files', ['index.html', 'static/**', 'webpack-output/**']);
2 |
3 | fis.match('static/**.js', {
4 | optimizer: fis.plugin('uglify-js')
5 | });
6 |
7 | fis.match('*.css', {
8 | optimizer: fis.plugin('clean-css')
9 | });
10 |
11 | fis.match('*.png', {
12 | optimizer: fis.plugin('png-compressor')
13 | });
14 |
15 | fis.match('*.{js,css,png}', {
16 | useHash: true,
17 | domain: 'http://somecdn.com',
18 | });
19 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React Redux Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { browserHistory } from 'react-router'
4 | import { syncHistoryWithStore } from 'react-router-redux'
5 | import store from './store'
6 | import Root from './containers/Root'
7 | import * as auth from './auth'
8 | import { initLeanCloud } from './utils'
9 |
10 | require('./static/styles/bootstrap.theme.scss')
11 |
12 | if (auth.loggedIn()) {
13 | initLeanCloud()
14 | }
15 |
16 | const history = syncHistoryWithStore(browserHistory, store)
17 |
18 | render(
19 | ,
20 | document.getElementById('react-container')
21 | )
22 |
--------------------------------------------------------------------------------
/models/Answer.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const Answer = AV.Object.extend('Answer')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default Answer
14 |
--------------------------------------------------------------------------------
/models/BroadcastSystemNotification.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 | import UserNotification from './UserNotification'
3 |
4 | const BroadcastSystemNotification = AV.Object.extend('BroadcastSystemNotification')
5 |
6 | /**
7 | * Class Methods
8 | */
9 |
10 | BroadcastSystemNotification.fetchAll = function (page = 1, perPage = 15) {
11 | const query = new AV.Query(BroadcastSystemNotification)
12 |
13 | //query.limit(perPage)
14 | //query.skip(perPage * (page - 1))
15 | query.addDescending('createdAt')
16 |
17 | return query.find()
18 | }
19 |
20 | BroadcastSystemNotification.push = function (content) {
21 | const query = new AV.Query(AV.User)
22 |
23 | // 若要发送给所有人, 则注释掉下一行
24 | //query.equalTo('objectId', "56713bcc60b2e41662ab72e6")
25 | return query.find().then(function (users) {
26 | const notifications = users.map((user) => {
27 | const notification = new UserNotification()
28 |
29 | notification.set('kind', 2)
30 | notification.set('content', content)
31 | notification.set('user', user)
32 |
33 | user.increment('unreadOtherNotificationsCount')
34 | user.save()
35 |
36 | return notification
37 | })
38 |
39 | return AV.Object.saveAll(notifications)
40 | }).then(function () {
41 | return AV.Push.send({
42 | data: {
43 | alert: content,
44 | badge: 'Increment',
45 | kind: '2'
46 | }
47 | });
48 | }).then(function () {
49 | const broadcastSystemNotifications = new BroadcastSystemNotification()
50 |
51 | broadcastSystemNotifications.set('content', content)
52 | return broadcastSystemNotifications.save()
53 | })
54 | }
55 |
56 | /**
57 | * Instance Methods
58 | */
59 |
60 | export default BroadcastSystemNotification
61 |
--------------------------------------------------------------------------------
/models/HomeRecommendation.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const HomeRecommendation = AV.Object.extend('HomeRecommendation')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | HomeRecommendation.fetchAll = function (page = 1, perPage = 15) {
10 | const query = new AV.Query(HomeRecommendation)
11 | const countQuery = new AV.Query(HomeRecommendation)
12 |
13 | query.addDescending('createdAt')
14 | query.limit(perPage)
15 | query.skip(perPage * (page - 1))
16 | query.include('user')
17 |
18 | return AV.Promise.when(query.find(), countQuery.count())
19 | }
20 |
21 | HomeRecommendation.add = function (kind, object) {
22 | const query = new AV.Query(HomeRecommendation)
23 |
24 | query.equalTo('kind', kind)
25 | if (kind === 'user') {
26 | query.equalTo('user', object)
27 | }
28 |
29 | return query.first().then(function (homeRecommendation) {
30 | if (homeRecommendation) {
31 | homeRecommendation.destroy()
32 | }
33 |
34 | const newHomeRecommendation = new HomeRecommendation()
35 | newHomeRecommendation.set('kind', kind)
36 |
37 | if (kind === 'user') {
38 | newHomeRecommendation.set('user', object)
39 | }
40 |
41 | return newHomeRecommendation.save()
42 | })
43 | }
44 |
45 | /**
46 | * Instance Methods
47 | */
48 |
49 | Object.assign(HomeRecommendation.prototype, {
50 | updateUserBackgroundImage(imageFile) {
51 | const user = this.get('user')
52 |
53 | return resizeImageFile(imageFile, 600, 300).then(function (imageBase64Data) {
54 | const avFile = new AV.File("backgroundImage.png", {base64: imageBase64Data})
55 |
56 | return avFile.save()
57 | }).then(function (fileObject) {
58 | user.set('userCardBackgroundImage', fileObject)
59 |
60 | return user.save()
61 | }).then(function () {
62 | return this
63 | })
64 | },
65 |
66 | top() {
67 | this.destroy()
68 | const homeRecommendation = new HomeRecommendation()
69 |
70 | homeRecommendation.set('kind', 'user')
71 | homeRecommendation.set('user', this.get('user'))
72 |
73 | return homeRecommendation.save()
74 | }
75 | })
76 |
77 | function resizeImageFile(imageFile, targetWidth, targetHeight) {
78 | const image = new Image()
79 | const canvas = document.createElement('canvas')
80 | const context = canvas.getContext('2d')
81 |
82 | canvas.style.visibility = "hidden"
83 | canvas.width = targetWidth
84 | canvas.height = targetHeight
85 | document.body.appendChild(canvas)
86 |
87 | return new AV.Promise(function (resolve, reject) {
88 | image.src = URL.createObjectURL(imageFile)
89 |
90 | image.onload = function () {
91 | if (this.width < targetWidth || this.height < targetHeight) {
92 | const alertString = `图片大小至少为 ${targetWidth} x ${targetHeight}`
93 |
94 | reject(alertString)
95 | window.alert(alertString)
96 | return
97 | }
98 |
99 | let sx, sy, sWidth, sHeight;
100 |
101 | if (this.width > 2 * this.height) {
102 | sWidth = 2 * this.height
103 | sHeight = this.height
104 | sx = (this.width - sWidth) / 2
105 | sy = 0
106 | } else {
107 | sWidth = this.width
108 | sHeight = sWidth / 2
109 | sx = 0
110 | sy = (this.height - sHeight) / 2
111 | }
112 |
113 | // See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
114 | context.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, targetWidth, targetHeight)
115 |
116 | const imageBase64Data = canvas.toDataURL().split('base64,')[1]
117 |
118 | resolve(imageBase64Data)
119 | }
120 | })
121 | }
122 |
123 | export default HomeRecommendation
124 |
--------------------------------------------------------------------------------
/models/InvitationCode.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const InvitationCode = AV.Object.extend('InvitationCode')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | InvitationCode.fetchAll = function (page = 1, perPage = 15) {
10 | const query = new AV.Query(InvitationCode)
11 | const countQuery = new AV.Query(InvitationCode)
12 |
13 | query.addDescending('createdAt')
14 | query.skip(perPage * (page - 1))
15 | query.limit(perPage)
16 | query.include('createdBy')
17 | query.include('user')
18 |
19 | return AV.Promise.when(query.find(), countQuery.count())
20 | }
21 |
22 | InvitationCode.generateCodes = function (count = 5) {
23 | return AV.Cloud.run('generateInvitationCodeByAdmin').then(function (codes) {
24 | return codes.map((code) => {
25 | // Convert pain Object to AV Object
26 | var object = AV.Object._create('InvitationCode')
27 | object._finishFetch(code, true)
28 | return object
29 | })
30 | })
31 | }
32 |
33 | /**
34 | * Instance Methods
35 | */
36 |
37 | Object.assign(InvitationCode.prototype, {
38 | markSended(sendedTo){
39 | this.set('sended', true)
40 | this.set('sendedAt', new Date())
41 | this.set('sendedTo', sendedTo)
42 |
43 | return this.save()
44 | }
45 | })
46 |
47 | export default InvitationCode
48 |
--------------------------------------------------------------------------------
/models/LikeQuestion.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const LikeQuestion = AV.Object.extend('LikeQuestion')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default LikeQuestion
14 |
--------------------------------------------------------------------------------
/models/Question.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 | import QuestionComment from './QuestionComment'
3 |
4 | const Question = AV.Object.extend('Question')
5 |
6 | /**
7 | * Class Methods
8 | */
9 |
10 | Question.fetchAll = function (page = 1, perPage = 15) {
11 | const query = new AV.Query(Question)
12 | const countQuery = new AV.Query(Question)
13 |
14 | query.limit(perPage)
15 | query.skip(perPage * (page - 1))
16 | query.include('asker')
17 | query.include('user')
18 | query.addDescending('createdAt')
19 |
20 | return AV.Promise.when(query.find(), countQuery.count())
21 | }
22 |
23 | Question.fetchPendingAnonymousQuestions = function (page = 1, perPage = 15) {
24 | const query = new AV.Query(Question)
25 | const countQuery = new AV.Query(Question)
26 |
27 | query.equalTo('anonymous', true)
28 | query.equalTo('answered', false)
29 | query.limit(perPage)
30 | query.skip(perPage * (page - 1))
31 | query.include('asker')
32 | query.include('user')
33 | query.addDescending('createdAt')
34 |
35 | countQuery.equalTo('anonymous', true)
36 | countQuery.equalTo('answered', false)
37 |
38 | return AV.Promise.when(query.find(), countQuery.count())
39 | }
40 |
41 | Question.fetchAnswers = function (page = 1, perPage = 15) {
42 | const query = new AV.Query(Question)
43 | const countQuery = new AV.Query(Question)
44 |
45 | query.equalTo('answered', true)
46 | query.limit(perPage)
47 | query.skip(perPage * (page - 1))
48 | query.include('asker')
49 | query.include('user')
50 | query.addDescending('answeredAt')
51 |
52 | countQuery.equalTo('answered', true)
53 |
54 | return AV.Promise.when(query.find(), countQuery.count())
55 | }
56 |
57 | Question.fetchTodayQuestionsCount = function () {
58 | const query = new AV.Query(Question)
59 | const now = new Date()
60 |
61 | now.setHours(0, 0, 0, 0)
62 | query.greaterThan('createdAt', now)
63 |
64 | return query.count()
65 | }
66 |
67 | Question.fetchTodayAnswersCount = function () {
68 | const query = new AV.Query(Question)
69 | const now = new Date()
70 |
71 | now.setHours(0, 0, 0, 0)
72 | query.greaterThan('answeredAt', now)
73 |
74 | return query.count()
75 | }
76 |
77 | /**
78 | * Instance Methods
79 | */
80 |
81 | Object.assign(Question.prototype, {
82 | update(title) {
83 | this.set('title', title)
84 | return this.save()
85 | },
86 |
87 | fetchComments(page = 1, perPage = 15) {
88 | const query = new AV.Query(QuestionComment)
89 |
90 | query.equalTo('question', this)
91 | query.addAscending('createdAt')
92 | query.limit(perPage)
93 | query.skip(perPage * (page - 1))
94 | query.include('user')
95 |
96 | return query.find()
97 | }
98 | })
99 |
100 | export default Question
101 |
--------------------------------------------------------------------------------
/models/QuestionComment.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const QuestionComment = AV.Object.extend('QuestionComment')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default QuestionComment
14 |
--------------------------------------------------------------------------------
/models/ReportQuestion.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const ReportQuestion = AV.Object.extend('ReportQuestion')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | ReportQuestion.fetchAll = function (page = 1, perPage = 15) {
10 | const query = new AV.Query(ReportQuestion)
11 | const countQuery = new AV.Query(ReportQuestion)
12 |
13 | query.include('reporter')
14 | query.include('question')
15 | query.include('question.user')
16 | query.include('question.asker')
17 | query.addDescending('createdAt')
18 | query.limit(perPage)
19 | query.skip(perPage * (page - 1))
20 |
21 | return AV.Promise.when(query.find(), countQuery.count())
22 | }
23 |
24 | ReportQuestion.fetchTodayReportedQuestionsCount = function () {
25 | const query = new AV.Query(ReportQuestion)
26 | const now = new Date()
27 |
28 | now.setHours(0, 0, 0, 0)
29 | query.greaterThan('createdAt', now)
30 |
31 | return query.count()
32 | }
33 |
34 | /**
35 | * Instance Methods
36 | */
37 |
38 | export default ReportQuestion
39 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 | import UserTagAssociation from './UserTagAssociation'
3 | import Question from './Question'
4 |
5 | const User = AV.User
6 |
7 | /**
8 | * Class Methods
9 | */
10 |
11 | User.fetchTodayUsersCount = function () {
12 | const query = new AV.Query(User)
13 | const now = new Date()
14 |
15 | now.setHours(0, 0, 0, 0)
16 | query.greaterThan('createdAt', now)
17 |
18 | return query.count()
19 | }
20 |
21 | User.askAnonymousQuestion = function (targetUser, title) {
22 | const question = new Question()
23 |
24 | question.fetchWhenSave(true)
25 | question.set('user', targetUser)
26 | question.set('anonymous', true)
27 | question.set('title', title)
28 |
29 | return question.save()
30 | }
31 |
32 | User.fetchAll = function (page = 1, perPage = 15) {
33 | const query = new AV.Query(User)
34 | const countQuery = new AV.Query(User)
35 |
36 | query.addDescending('createdAt')
37 | query.skip(perPage * (page - 1))
38 | query.limit(perPage)
39 |
40 | return AV.Promise.when(query.find(), countQuery.count())
41 | }
42 |
43 | User.searchByName = function (name, page = 1, perPage = 15) {
44 | const query = new AV.Query(User)
45 | const countQuery = new AV.Query(User)
46 |
47 | query.contains('name', name)
48 | query.skip(perPage * (page - 1))
49 | query.limit(perPage)
50 |
51 | countQuery.contains('name', name)
52 |
53 | return AV.Promise.when(query.find(), countQuery.count())
54 | }
55 |
56 | User.searchOpenedUserByName = function (name, page = 1, perPage = 15) {
57 | const query = new AV.Query(User)
58 | const countQuery = new AV.Query(User)
59 |
60 | query.contains('name', name)
61 | query.equalTo('opened', true)
62 | query.skip(perPage * (page - 1))
63 | query.limit(perPage)
64 |
65 | countQuery.contains('name', name)
66 | countQuery.equalTo('opened', true)
67 |
68 | return AV.Promise.when(query.find(), countQuery.count())
69 | }
70 |
71 | /**
72 | * Instance Methods
73 | */
74 |
75 | Object.assign(User.prototype, {
76 | fetchQuestionsByLikesCount(page = 1, perPage = 15) {
77 | const query = new AV.Query(Question)
78 |
79 | query.limit(perPage)
80 | query.skip(perPage * (page - 1))
81 | query.equalTo('user', this)
82 | query.equalTo('answered', true)
83 | query.include('asker')
84 | query.include('user')
85 | query.addDescending('likesCount')
86 |
87 | return query.find()
88 | },
89 |
90 | fetchQuestionsByTime(page = 1, perPage = 15) {
91 | const query = new AV.Query(Question)
92 |
93 | query.limit(perPage)
94 | query.skip(perPage * (page - 1))
95 | query.equalTo('user', this)
96 | query.equalTo('answered', true)
97 | query.include('asker')
98 | query.include('user')
99 | query.addDescending('createdAt')
100 |
101 | return query.find()
102 | },
103 |
104 | fetchPendingQuestions(page = 1, perPage = 15) {
105 | const query = new AV.Query(Question)
106 |
107 | query.limit(perPage)
108 | query.skip(perPage * (page - 1))
109 | query.equalTo('user', this)
110 | query.equalTo('answered', false)
111 | query.include('asker')
112 | query.include('user')
113 | query.addDescending('createdAt')
114 |
115 | return query.find()
116 | },
117 |
118 | fetchDrafts(page = 1, perPage = 15){
119 | const query = new AV.Query(Question)
120 |
121 | query.limit(perPage)
122 | query.skip(perPage * (page - 1))
123 | query.equalTo('user', this)
124 | query.equalTo('drafted', true)
125 | query.include('asker')
126 | query.include('user')
127 | query.addDescending('draftedAt')
128 |
129 | return query.find()
130 | },
131 |
132 | fetchAskedQuestions(page = 1, perPage = 15){
133 | const query = new AV.Query(Question)
134 |
135 | query.limit(perPage)
136 | query.skip(perPage * (page - 1))
137 | query.equalTo('asker', this)
138 | query.include('asker')
139 | query.include('user')
140 | query.addDescending('createdAt')
141 |
142 | return query.find()
143 | },
144 |
145 | openQAPage() {
146 | this.set('authed', true)
147 | this.set('opened', true)
148 | this.set('processingQARequest', false)
149 |
150 | return this.save()
151 | },
152 |
153 | closeQAPage() {
154 | this.set('authed', false)
155 | this.set('opened', false)
156 | this.set('processingQARequest', false)
157 |
158 | return this.save()
159 | },
160 |
161 | update(name, title, achievements, desc, mobilePhoneNumber, tags){
162 | // 删除该用户的 userTagAssociations
163 | const query = new AV.Query(UserTagAssociation)
164 | query.equalTo('user', this)
165 | query.destroyAll()
166 |
167 | this.set('name', name)
168 | this.set('title', title)
169 | this.set('achievements', achievements)
170 | this.set('desc', desc)
171 | this.set('mobilePhoneNumber', mobilePhoneNumber)
172 | this.set('tags', tags)
173 |
174 | return this.save().then(function (user) {
175 | // 新建 userTagAssociations
176 | AV.Object.saveAll(tags.map((tag) => {
177 | const userTagAssociation = new UserTagAssociation()
178 | userTagAssociation.set('tag', tag)
179 | userTagAssociation.set('user', user)
180 | return userTagAssociation
181 | }))
182 |
183 | return user
184 | })
185 | }
186 | })
187 |
188 | export default User
189 |
--------------------------------------------------------------------------------
/models/UserNotification.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const UserNotification = AV.Object.extend('UserNotification')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default UserNotification
14 |
--------------------------------------------------------------------------------
/models/UserNotificationSender.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const UserNotificationSender = AV.Object.extend('UserNotificationSender')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default UserNotificationSender
14 |
--------------------------------------------------------------------------------
/models/UserTag.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const UserTag = AV.Object.extend('UserTag')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default UserTag
14 |
--------------------------------------------------------------------------------
/models/UserTagAssociation.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | const UserTagAssociation = AV.Object.extend('UserTagAssociation')
4 |
5 | /**
6 | * Class Methods
7 | */
8 |
9 | /**
10 | * Instance Methods
11 | */
12 |
13 | export default UserTagAssociation
14 |
--------------------------------------------------------------------------------
/models/index.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 | import Answer from './Answer'
3 | import HomeRecommendation from './HomeRecommendation'
4 | import InvitationCode from './InvitationCode'
5 | import LikeQuestion from './LikeQuestion'
6 | import Question from './Question'
7 | import QuestionComment from './QuestionComment'
8 | import ReportQuestion from './ReportQuestion'
9 | import User from './User'
10 | import UserNotification from './UserNotification'
11 | import UserNotificationSender from './UserNotificationSender'
12 | import UserTag from './UserTag'
13 | import UserTagAssociation from './UserTagAssociation'
14 | import BroadcastSystemNotification from './BroadcastSystemNotification'
15 |
16 | export { Answer, HomeRecommendation, InvitationCode, LikeQuestion, Question, QuestionComment, ReportQuestion,
17 | User, UserNotification, UserNotificationSender, UserTag, UserTagAssociation, BroadcastSystemNotification}
18 |
19 | export const fetchDashboardData = function () {
20 | return AV.Promise.when(User.fetchTodayUsersCount(), Question.fetchTodayQuestionsCount(),
21 | Question.fetchTodayAnswersCount(), ReportQuestion.fetchTodayReportedQuestionsCount())
22 | }
23 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 6688;
3 | server_name admin.上山打老虎.com;
4 | root /var/www/react-redux-example/output;
5 |
6 | location ~* \.(?:css|png|js)$ {
7 | expires max;
8 | add_header Cache-Control "public";
9 | }
10 |
11 | location / {
12 | try_files $uri /index.html;
13 | }
14 | }
15 |
16 | server {
17 | listen 80;
18 | server_name www.上山打老虎.com;
19 | return 301 http://上山打老虎.com;
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "build": "NODE_ENV=production webpack --progress --config webpack.config.prod.js && fis3 release -d ./output",
9 | "deploy": "fab deploy -i ~/.ssh/kp-******"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/hustlzp/react-redux-example.git"
14 | },
15 | "author": "hustlzp",
16 | "license": "ISC",
17 | "devDependencies": {
18 | "avoscloud-sdk": "^0.6.9",
19 | "babel-core": "^6.4.0",
20 | "babel-loader": "^6.2.3",
21 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
22 | "babel-plugin-transform-object-rest-spread": "^6.5.0",
23 | "babel-preset-es2015": "^6.3.13",
24 | "babel-preset-react": "^6.5.0",
25 | "babel-preset-react-hmre": "^1.1.0",
26 | "babel-preset-stage-1": "^6.5.0",
27 | "bootstrap": "^3.3.6",
28 | "css-loader": "^0.23.1",
29 | "font-awesome": "^4.5.0",
30 | "hint.css": "^2.1.0",
31 | "history": "^1.17.0",
32 | "jsx-control-statements": "^3.1.0",
33 | "lodash": "^4.5.1",
34 | "moment": "^2.11.2",
35 | "node-sass": "^3.4.2",
36 | "normalize.css": "^3.0.3",
37 | "radium": "^0.16.6",
38 | "react": "^0.14.6",
39 | "react-addons-update": "^0.14.7",
40 | "react-dom": "^0.14.6",
41 | "react-modal": "^0.6.1",
42 | "react-redux": "^4.0.6",
43 | "react-router": "^2.0.0",
44 | "react-router-redux": "^4.0.0",
45 | "react-tabs": "^0.5.3",
46 | "redux": "^3.0.5",
47 | "redux-devtools": "^3.1.1",
48 | "redux-devtools-dock-monitor": "^1.1.0",
49 | "redux-devtools-log-monitor": "^1.0.4",
50 | "redux-logger": "^2.6.0",
51 | "redux-thunk": "^1.0.3",
52 | "sass-loader": "^3.1.2",
53 | "style-loader": "^0.13.0",
54 | "webpack": "^1.12.14",
55 | "webpack-dev-middleware": "^1.5.1",
56 | "webpack-hot-middleware": "^2.8.1",
57 | "express": "^4.13.4",
58 | "whatwg-fetch": "^0.11.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/qrsync.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "src": "output",
3 | "dest": "qiniu:access_key=12345&secret_key=上山打老虎",
4 | "debug_level": 0
5 | }
--------------------------------------------------------------------------------
/reducers/account.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import { createReducer } from '../utils'
3 | import * as auth from '../auth'
4 | import * as actionType from '../actions/account'
5 |
6 | export default createReducer({authed: auth.loggedIn()}, {
7 | // 登录
8 | [actionType.LOG_IN_START](state, action) {
9 | return state
10 | },
11 | [actionType.LOG_IN_SUCCESS](state, action) {
12 | return update(state, {authed: {$set: true}})
13 | },
14 | [actionType.LOG_IN_ERROR](state, action) {
15 | return state
16 | },
17 |
18 | // 登出
19 | [actionType.LOG_OUT](state, action) {
20 | return update(state, {authed: {$set: false}})
21 | },
22 |
23 | })
24 |
--------------------------------------------------------------------------------
/reducers/answers.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/question'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, answers: []}, {
6 | // 获取已回答问题
7 | [actionType.FETCH_ANSWERS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_ANSWERS_SUCCESS](state, action) {
11 | return update(state, {
12 | currentPage: {$set: action.page},
13 | totalCount: {$set: action.totalCount},
14 | answers: {$set: action.answers}
15 | })
16 | },
17 | [actionType.FETCH_ANSWERS_ERROR](state, action) {
18 | return state
19 | },
20 |
21 | // 更新问题
22 | [actionType.UPDATE_QUESTION_START](state, action) {
23 | return state
24 | },
25 | [actionType.UPDATE_QUESTION_SUCCESS](state, action) {
26 | return update(state, {
27 | answers: {
28 | $set: state.answers.map((question) => {
29 | return question.id === action.question.id ? action.question : question
30 | })
31 | }
32 | })
33 | },
34 | [actionType.UPDATE_QUESTION_ERROR](state, action){
35 | return state
36 | },
37 |
38 | // 删除提问
39 | [actionType.DELETE_QUESTION_START](state, action) {
40 | return state
41 | },
42 | [actionType.DELETE_QUESTION_SUCCESS](state, action) {
43 | return update(state, {
44 | answers: {
45 | $set: state.answers.filter(question => question.id !== action.question.id)
46 | }
47 | })
48 | },
49 | [actionType.DELETE_QUESTION_ERROR](state, action) {
50 | return state
51 | },
52 | })
53 |
--------------------------------------------------------------------------------
/reducers/broadcastSystemNotifications.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/broadcastSystemNotifications'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer([], {
6 | // 获取系统通知
7 | [actionType.FETCH_BROADCAST_SYSTEM_NOTIFICATIONS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_BROADCAST_SYSTEM_NOTIFICATIONS_SUCCESS](state, action){
11 | return [...action.notifications]
12 | },
13 | [actionType.FETCH_BROADCAST_SYSTEM_NOTIFICATIONS_ERROR](state, action) {
14 | return state
15 | },
16 |
17 | // 发送广播系统通知
18 | [actionType.PUSH_BROADCAST_SYSTEM_NOTIFICATION_START](state, action) {
19 | return state
20 | },
21 | [actionType.PUSH_BROADCAST_SYSTEM_NOTIFICATION_SUCCESS](state, action) {
22 | return update(state, {$unshift: [action.notification]})
23 | },
24 | [actionType.PUSH_BROADCAST_SYSTEM_NOTIFICATION_ERROR](state, action) {
25 | return state
26 | },
27 | })
28 |
--------------------------------------------------------------------------------
/reducers/dashboard.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/dashboard'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({usersCount: 0, questionsCount: 0, answersCount: 0, reportsCount: 0}, {
6 | // 获取Dashboard数据
7 | [actionType.FETCH_DASHBOARD_DATA_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_DASHBOARD_DATA_SUCCESS](state, action) {
11 | return update(state, {$merge: {...action}})
12 | },
13 | [actionType.FETCH_DASHBOARD_DATA_ERROR](state, action) {
14 | return state
15 | },
16 | })
17 |
--------------------------------------------------------------------------------
/reducers/homeRecommendations.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/homeRecommendations'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, recommendations: {}}, {
6 | // 获取首页推荐
7 | [actionType.FETCH_HOME_RECOMMENDATIONS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_HOME_RECOMMENDATIONS_SUCCESS](state, action) {
11 | return update(state, {
12 | totalCount: {$set: action.totalCount},
13 | currentPage: {$set: action.page},
14 | recommendations: {[action.page]: {$set: action.recommendations}}
15 | })
16 | },
17 | [actionType.FETCH_HOME_RECOMMENDATIONS_ERROR] (state, action){
18 | return state
19 | },
20 |
21 | // 移除首页推荐
22 | [actionType.REMOVE_HOME_RECOMMENDATION_START](state, action) {
23 | return state
24 | },
25 | [actionType.REMOVE_HOME_RECOMMENDATION_SUCCESS](state, action) {
26 | return update(state, {
27 | recommendations: {
28 | [state.currentPage]: {
29 | $set: state.recommendations[state.currentPage].filter((recommendation) => {
30 | return recommendation.id !== action.recommendation.id
31 | })
32 | }
33 | }
34 | })
35 | },
36 | [actionType.REMOVE_HOME_RECOMMENDATION_ERROR](state, action) {
37 | return state
38 | },
39 |
40 | // 更新首页推荐用户的背景图片
41 | [actionType.UPDATE_HOME_RECOMMENDATION_USER_BACKGROUND_IMAGE_START](state, action){
42 | return state
43 | },
44 | [actionType.UPDATE_HOME_RECOMMENDATION_USER_BACKGROUND_IMAGE_SUCCESS](state, action) {
45 | return update(state, {
46 | recommendations: {
47 | [state.currentPage]: {
48 | $set: state.recommendations[state.currentPage].map((recommendation) => {
49 | return recommendation.id === action.recommendation.id ? action.recommendation : recommendation
50 | })
51 | }
52 | }
53 | })
54 | },
55 | [actionType.UPDATE_HOME_RECOMMENDATION_USER_BACKGROUND_IMAGE_ERROR](state, action){
56 | return state
57 | },
58 |
59 | // 添加首页推荐
60 | [actionType.ADD_HOME_RECOMMENDATION_START](state, action) {
61 | return state
62 | },
63 | [actionType.ADD_HOME_RECOMMENDATION_SUCCESS](state, action) {
64 | let newState = update(state, {
65 | currentPage: {$set: 1},
66 | recommendations: {
67 | [state.currentPage]: {
68 | $set: state.recommendations[state.currentPage].filter((recommendation) => {
69 | return recommendation.id !== action.recommendation.id
70 | })
71 | }
72 | }
73 | })
74 |
75 | return update(newState, {
76 | recommendations: {1: {$unshift: [action.recommendation]}}
77 | })
78 | },
79 | [actionType.ADD_HOME_RECOMMENDATION_ERROR](state, action) {
80 | return state
81 | },
82 |
83 | // 置顶首页推荐
84 | [actionType.TOP_HOME_RECOMMENDATION_START](state, action) {
85 | return state
86 | },
87 | [actionType.TOP_HOME_RECOMMENDATION_SUCCESS](state, action) {
88 | let newState = update(state, {
89 | currentPage: {$set: 1},
90 | recommendations: {
91 | [state.currentPage]: {
92 | $set: state.recommendations[state.currentPage].filter((recommendation) => {
93 | return recommendation.id !== action.recommendation.id
94 | })
95 | }
96 | }
97 | })
98 |
99 | return update(newState, {
100 | recommendations: {1: {$unshift: [action.recommendation]}}
101 | })
102 | },
103 | [actionType.TOP_HOME_RECOMMENDATION_ERROR](state, action) {
104 | return state
105 | },
106 | })
107 |
--------------------------------------------------------------------------------
/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { routerReducer } from 'react-router-redux'
3 | import account from './account'
4 | import users from './users'
5 | import invitationCodes from './invitationCodes'
6 | import homeRecommendations from './homeRecommendations'
7 | import reportedQuestions from './reportedQuestions'
8 | import pendingAnonymousQuestions from './pendingAnonymousQuestions'
9 | import broadcastSystemNotifications from './broadcastSystemNotifications'
10 | import questions from './questions'
11 | import answers from './answers'
12 | import dashboard from './dashboard'
13 |
14 | const rootReducer = combineReducers({
15 | routing: routerReducer,
16 | dashboard,
17 | account,
18 | users,
19 | invitationCodes,
20 | homeRecommendations,
21 | reportedQuestions,
22 | pendingAnonymousQuestions,
23 | broadcastSystemNotifications,
24 | questions,
25 | answers
26 | })
27 |
28 | export default rootReducer
--------------------------------------------------------------------------------
/reducers/invitationCodes.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/invitationCodes'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, codes: {}}, {
6 | // 获取验证码
7 | [actionType.FETCH_INVITATION_CODES_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_INVITATION_CODES_SUCCESS](state, action) {
11 | return update(state, {
12 | totalCount: {$set: action.totalCount},
13 | currentPage: {$set: action.page},
14 | codes: {[action.page]: {$set: action.codes}}
15 | })
16 | },
17 | [actionType.FETCH_INVITATION_CODES_ERROR](state, action){
18 | return state
19 | },
20 |
21 | // 标记验证码为已读
22 | [actionType.MARK_INVITATION_CODE_SENDED_START](state, action) {
23 | return state
24 | },
25 | [actionType.MARK_INVITATION_CODE_SENDED_SUCCESS](state, action) {
26 | return update(state, {
27 | codes: {
28 | [state.currentPage]: {
29 | $set: state.codes[state.currentPage].map((code) => {
30 | return code.id === action.code.id ? action.code : code
31 | })
32 | }
33 | }
34 | })
35 | },
36 | [actionType.MARK_INVITATION_CODE_SENDED_ERROR](state, action){
37 | return state
38 | },
39 |
40 | // 生成新的邀请码
41 | [actionType.GENERATE_INVITATION_CODES_START](state, action) {
42 | return state
43 | },
44 | [actionType.GENERATE_INVITATION_CODES_SUCCESS](state, action) {
45 | return update(state, {
46 | totalCount: {$apply: count => count + action.count},
47 | currentPage: {$set: 1},
48 | codes: {1: {$unshift: action.codes}}
49 | }
50 | )
51 | },
52 | [actionType.GENERATE_INVITATION_CODES_ERROR](state, action){
53 | return state
54 | },
55 |
56 | })
57 |
--------------------------------------------------------------------------------
/reducers/pendingAnonymousQuestions.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/question'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, questions: []}, {
6 | // 获取匿名提问
7 | [actionType.FETCH_PENDING_ANONYMOUS_QUESTIONS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_PENDING_ANONYMOUS_QUESTIONS_SUCCESS](state, action) {
11 | return update(state, {
12 | currentPage: {$set: action.page},
13 | totalCount: {$set: action.totalCount},
14 | questions: {$set: action.questions}
15 | })
16 | },
17 | [actionType.FETCH_PENDING_ANONYMOUS_QUESTIONS_ERROR](state, action){
18 | return state
19 | },
20 |
21 | // 更新问题
22 | [actionType.UPDATE_QUESTION_START](state, action) {
23 | return state
24 | },
25 | [actionType.UPDATE_QUESTION_SUCCESS](state, action) {
26 | return update(state, {
27 | questions: {
28 | $set: state.questions.map((question) => {
29 | return question.id === action.question.id ? action.question : question
30 | })
31 | }
32 | })
33 | },
34 | [actionType.UPDATE_QUESTION_ERROR](state, action){
35 | return state
36 | },
37 |
38 | // 删除提问
39 | [actionType.DELETE_QUESTION_START](state, action) {
40 | return state
41 | },
42 | [actionType.DELETE_QUESTION_SUCCESS](state, action) {
43 | return update(state, {
44 | questions: {
45 | $set: state.questions.filter(question => question.id !== action.question.id)
46 | }
47 | })
48 | },
49 | [actionType.DELETE_QUESTION_ERROR](state, action) {
50 | return state
51 | },
52 |
53 | })
54 |
--------------------------------------------------------------------------------
/reducers/questions.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/question'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, questions: []}, {
6 | // 获取问题
7 | [actionType.FETCH_QUESTIONS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_QUESTIONS_SUCCESS](state, action) {
11 | return update(state, {
12 | currentPage: {$set: action.page},
13 | totalCount: {$set: action.totalCount},
14 | questions: {$set: action.questions}
15 | })
16 | },
17 | [actionType.FETCH_QUESTIONS_ERROR](state, action){
18 | return state
19 | },
20 |
21 | // 更新问题
22 | [actionType.UPDATE_QUESTION_START](state, action) {
23 | return state
24 | },
25 | [actionType.UPDATE_QUESTION_SUCCESS](state, action) {
26 | return update(state, {
27 | questions: {
28 | $set: state.questions.map((question) => {
29 | return question.id === action.question.id ? action.question : question
30 | })
31 | }
32 | })
33 | },
34 | [actionType.UPDATE_QUESTION_ERROR](state, action){
35 | return state
36 | },
37 |
38 | // 删除提问
39 | [actionType.DELETE_QUESTION_START](state, action) {
40 | return state
41 | },
42 | [actionType.DELETE_QUESTION_SUCCESS](state, action) {
43 | return update(state, {
44 | questions: {
45 | $set: state.questions.filter(question => question.id !== action.question.id)
46 | }
47 | })
48 | },
49 | [actionType.DELETE_QUESTION_ERROR](state, action) {
50 | return state
51 | },
52 |
53 | })
54 |
--------------------------------------------------------------------------------
/reducers/reportedQuestions.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as reportActionType from '../actions/reportedQuestions'
3 | import * as questionActionType from '../actions/question'
4 | import { createReducer } from '../utils'
5 |
6 | export default createReducer({totalCount: 0, currentPage: 1, reportedQuestions: []}, {
7 | // 获取举报问题
8 | [reportActionType.FETCH_REPORTED_QUESTIONS_START](state, action) {
9 | return state
10 | },
11 | [reportActionType.FETCH_REPORTED_QUESTIONS_SUCCESS](state, action) {
12 | return update(state, {
13 | currentPage: {$set: action.page},
14 | totalCount: {$set: action.totalCount},
15 | reportedQuestions: {$set: action.questions}
16 | })
17 | },
18 | [reportActionType.FETCH_REPORTED_QUESTIONS_ERROR](state, action){
19 | return state
20 | },
21 |
22 | // 删除提问
23 | [questionActionType.DELETE_QUESTION_START](state, action) {
24 | return state
25 | },
26 | [questionActionType.DELETE_QUESTION_SUCCESS](state, action) {
27 | return update(state, {
28 | reportedQuestions: {
29 | $set: state.reportedQuestions.filter(report => report.get('question').id !== action.question.id)
30 | }
31 | })
32 | },
33 | [questionActionType.DELETE_QUESTION_ERROR](state, action) {
34 | return state
35 | },
36 |
37 | // 更新问题
38 | [questionActionType.UPDATE_QUESTION_START](state, action) {
39 | return state
40 | },
41 | [questionActionType.UPDATE_QUESTION_SUCCESS](state, action) {
42 | return update(state, {
43 | reportedQuestions: {
44 | $set: state.reportedQuestions.map((report) => {
45 | if (report.get('question').id === action.question.id) {
46 | return update(report, {attributes: {question: {$set: action.question}}})
47 | } else {
48 | return report
49 | }
50 | })
51 | }
52 | })
53 | },
54 | [questionActionType.UPDATE_QUESTION_ERROR](state, action){
55 | return state
56 | },
57 |
58 | })
59 |
--------------------------------------------------------------------------------
/reducers/users.js:
--------------------------------------------------------------------------------
1 | import update from 'react-addons-update'
2 | import * as actionType from '../actions/users'
3 | import { createReducer } from '../utils'
4 |
5 | export default createReducer({totalCount: 0, currentPage: 1, users: []}, {
6 | // 获取用户
7 | [actionType.FETCH_USERS_START](state, action) {
8 | return state
9 | },
10 | [actionType.FETCH_USERS_SUCCESS](state, action) {
11 | return update(state, {
12 | currentPage: {$set: action.page},
13 | totalCount: {$set: action.totalCount},
14 | users: {$set: action.users}
15 | })
16 | },
17 | [actionType.FETCH_USERS_ERROR](state, action) {
18 | return state
19 | },
20 |
21 | // 开通问答主页
22 | [actionType.OPEN_USER_QA_PAGE_START](state, action) {
23 | return state
24 | },
25 | [actionType.OPEN_USER_QA_PAGE_SUCCESS](state, action) {
26 | return update(state, {
27 | users: {
28 | $set: state.users.map((user) => {
29 | return user.id === action.user.id ? action.user : user
30 | })
31 | }
32 | })
33 | },
34 | [actionType.OPEN_USER_QA_PAGE_ERROR](state, action) {
35 | return state
36 | },
37 |
38 | // 关闭问答主页
39 | [actionType.CLOSE_USER_QA_PAGE_START](state, action) {
40 | return state
41 | },
42 | [actionType.CLOSE_USER_QA_PAGE_SUCCESS](state, action) {
43 | return update(state, {
44 | users: {
45 | $set: state.users.map((user) => {
46 | return user.id === action.user.id ? action.user : user
47 | })
48 | }
49 | })
50 | },
51 | [actionType.CLOSE_USER_QA_PAGE_ERROR](state, action) {
52 | return state
53 | },
54 |
55 | // 更新用户
56 | [actionType.UPDATE_USER_START](state, action) {
57 | return state
58 | },
59 | [actionType.UPDATE_USER_SUCCESS](state, action) {
60 | return update(state, {
61 | users: {
62 | $set: state.map((user) => {
63 | return user.id === action.user.id ? action.user : user
64 | })
65 | }
66 | })
67 | },
68 | [actionType.UPDATE_USER_ERROR](state, action) {
69 | return state
70 | },
71 |
72 | // 搜索用户
73 | [actionType.SEARCH_USERS_START](state, action) {
74 | return state
75 | },
76 | [actionType.SEARCH_USERS_SUCCESS](state, action) {
77 | return update(state, {
78 | users: {$set: action.users}
79 | })
80 | },
81 | [actionType.SEARCH_USERS_ERROR](state, action) {
82 | return state
83 | },
84 |
85 | })
86 |
--------------------------------------------------------------------------------
/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, IndexRoute } from 'react-router'
3 | import App from './containers/App'
4 | import AuthPage from './containers/AuthPage'
5 | import PendingAnonymousQuestionsPage from './containers/PendingAnonymousQuestionsPage'
6 | import DashboardPage from './containers/DashboardPage'
7 | import HomeRecommendationsPage from './containers/HomeRecommendationsPage'
8 | import InvitationCodesPage from './containers/InvitationCodesPage'
9 | import QuestionsPage from './containers/QuestionsPage'
10 | import AnswersPage from './containers/AnswersPage'
11 | import BroadcastSystemNotificationsPage from './containers/BroadcastSystemNotificationsPage'
12 | import ReportsPage from './containers/ReportsPage'
13 | import UsersPage from './containers/UsersPage'
14 | import UserPage from './containers/UserPage'
15 | import VisitorQuestionsReviewPage from './containers/VisitorQuestionsReviewPage'
16 | import * as auth from './auth'
17 |
18 | function requireAuth(nextState, replace) {
19 | if (!auth.loggedIn()) {
20 | replace('/auth')
21 | }
22 | }
23 |
24 | export default (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | )
41 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var express = require('express');
3 | var webpackDevMiddleware = require('webpack-dev-middleware');
4 | var webpackHotMiddleware = require('webpack-hot-middleware');
5 | var config = require('./webpack.config.dev');
6 |
7 | var app = express();
8 | var port = 3000;
9 |
10 | app.use('/static', express.static('static'));
11 |
12 | var compiler = webpack(config);
13 | app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath}));
14 | app.use(webpackHotMiddleware(compiler));
15 |
16 | app.use(function (req, res) {
17 | res.sendFile(__dirname + '/index.html')
18 | });
19 |
20 | app.listen(port, function (error) {
21 | if (error) {
22 | console.error(error)
23 | } else {
24 | console.info("Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/static/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/static/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hustlzp/react-redux-example/9335dc690d584a150ca2d1d76f99a2d92faee5a1/static/images/logo.png
--------------------------------------------------------------------------------
/static/scripts/fetch.js:
--------------------------------------------------------------------------------
1 | (function(self) {
2 | 'use strict';
3 |
4 | if (self.fetch) {
5 | return
6 | }
7 |
8 | function normalizeName(name) {
9 | if (typeof name !== 'string') {
10 | name = String(name)
11 | }
12 | if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
13 | throw new TypeError('Invalid character in header field name')
14 | }
15 | return name.toLowerCase()
16 | }
17 |
18 | function normalizeValue(value) {
19 | if (typeof value !== 'string') {
20 | value = String(value)
21 | }
22 | return value
23 | }
24 |
25 | function Headers(headers) {
26 | this.map = {}
27 |
28 | if (headers instanceof Headers) {
29 | headers.forEach(function(value, name) {
30 | this.append(name, value)
31 | }, this)
32 |
33 | } else if (headers) {
34 | Object.getOwnPropertyNames(headers).forEach(function(name) {
35 | this.append(name, headers[name])
36 | }, this)
37 | }
38 | }
39 |
40 | Headers.prototype.append = function(name, value) {
41 | name = normalizeName(name)
42 | value = normalizeValue(value)
43 | var list = this.map[name]
44 | if (!list) {
45 | list = []
46 | this.map[name] = list
47 | }
48 | list.push(value)
49 | }
50 |
51 | Headers.prototype['delete'] = function(name) {
52 | delete this.map[normalizeName(name)]
53 | }
54 |
55 | Headers.prototype.get = function(name) {
56 | var values = this.map[normalizeName(name)]
57 | return values ? values[0] : null
58 | }
59 |
60 | Headers.prototype.getAll = function(name) {
61 | return this.map[normalizeName(name)] || []
62 | }
63 |
64 | Headers.prototype.has = function(name) {
65 | return this.map.hasOwnProperty(normalizeName(name))
66 | }
67 |
68 | Headers.prototype.set = function(name, value) {
69 | this.map[normalizeName(name)] = [normalizeValue(value)]
70 | }
71 |
72 | Headers.prototype.forEach = function(callback, thisArg) {
73 | Object.getOwnPropertyNames(this.map).forEach(function(name) {
74 | this.map[name].forEach(function(value) {
75 | callback.call(thisArg, value, name, this)
76 | }, this)
77 | }, this)
78 | }
79 |
80 | function consumed(body) {
81 | if (body.bodyUsed) {
82 | return Promise.reject(new TypeError('Already read'))
83 | }
84 | body.bodyUsed = true
85 | }
86 |
87 | function fileReaderReady(reader) {
88 | return new Promise(function(resolve, reject) {
89 | reader.onload = function() {
90 | resolve(reader.result)
91 | }
92 | reader.onerror = function() {
93 | reject(reader.error)
94 | }
95 | })
96 | }
97 |
98 | function readBlobAsArrayBuffer(blob) {
99 | var reader = new FileReader()
100 | reader.readAsArrayBuffer(blob)
101 | return fileReaderReady(reader)
102 | }
103 |
104 | function readBlobAsText(blob) {
105 | var reader = new FileReader()
106 | reader.readAsText(blob)
107 | return fileReaderReady(reader)
108 | }
109 |
110 | var support = {
111 | blob: 'FileReader' in self && 'Blob' in self && (function() {
112 | try {
113 | new Blob();
114 | return true
115 | } catch(e) {
116 | return false
117 | }
118 | })(),
119 | formData: 'FormData' in self,
120 | arrayBuffer: 'ArrayBuffer' in self
121 | }
122 |
123 | function Body() {
124 | this.bodyUsed = false
125 |
126 |
127 | this._initBody = function(body) {
128 | this._bodyInit = body
129 | if (typeof body === 'string') {
130 | this._bodyText = body
131 | } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
132 | this._bodyBlob = body
133 | } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
134 | this._bodyFormData = body
135 | } else if (!body) {
136 | this._bodyText = ''
137 | } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) {
138 | // Only support ArrayBuffers for POST method.
139 | // Receiving ArrayBuffers happens via Blobs, instead.
140 | } else {
141 | throw new Error('unsupported BodyInit type')
142 | }
143 |
144 | if (!this.headers.get('content-type')) {
145 | if (typeof body === 'string') {
146 | this.headers.set('content-type', 'text/plain;charset=UTF-8')
147 | } else if (this._bodyBlob && this._bodyBlob.type) {
148 | this.headers.set('content-type', this._bodyBlob.type)
149 | }
150 | }
151 | }
152 |
153 | if (support.blob) {
154 | this.blob = function() {
155 | var rejected = consumed(this)
156 | if (rejected) {
157 | return rejected
158 | }
159 |
160 | if (this._bodyBlob) {
161 | return Promise.resolve(this._bodyBlob)
162 | } else if (this._bodyFormData) {
163 | throw new Error('could not read FormData body as blob')
164 | } else {
165 | return Promise.resolve(new Blob([this._bodyText]))
166 | }
167 | }
168 |
169 | this.arrayBuffer = function() {
170 | return this.blob().then(readBlobAsArrayBuffer)
171 | }
172 |
173 | this.text = function() {
174 | var rejected = consumed(this)
175 | if (rejected) {
176 | return rejected
177 | }
178 |
179 | if (this._bodyBlob) {
180 | return readBlobAsText(this._bodyBlob)
181 | } else if (this._bodyFormData) {
182 | throw new Error('could not read FormData body as text')
183 | } else {
184 | return Promise.resolve(this._bodyText)
185 | }
186 | }
187 | } else {
188 | this.text = function() {
189 | var rejected = consumed(this)
190 | return rejected ? rejected : Promise.resolve(this._bodyText)
191 | }
192 | }
193 |
194 | if (support.formData) {
195 | this.formData = function() {
196 | return this.text().then(decode)
197 | }
198 | }
199 |
200 | this.json = function() {
201 | return this.text().then(JSON.parse)
202 | }
203 |
204 | return this
205 | }
206 |
207 | // HTTP methods whose capitalization should be normalized
208 | var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
209 |
210 | function normalizeMethod(method) {
211 | var upcased = method.toUpperCase()
212 | return (methods.indexOf(upcased) > -1) ? upcased : method
213 | }
214 |
215 | function Request(input, options) {
216 | options = options || {}
217 | var body = options.body
218 | if (Request.prototype.isPrototypeOf(input)) {
219 | if (input.bodyUsed) {
220 | throw new TypeError('Already read')
221 | }
222 | this.url = input.url
223 | this.credentials = input.credentials
224 | if (!options.headers) {
225 | this.headers = new Headers(input.headers)
226 | }
227 | this.method = input.method
228 | this.mode = input.mode
229 | if (!body) {
230 | body = input._bodyInit
231 | input.bodyUsed = true
232 | }
233 | } else {
234 | this.url = input
235 | }
236 |
237 | this.credentials = options.credentials || this.credentials || 'omit'
238 | if (options.headers || !this.headers) {
239 | this.headers = new Headers(options.headers)
240 | }
241 | this.method = normalizeMethod(options.method || this.method || 'GET')
242 | this.mode = options.mode || this.mode || null
243 | this.referrer = null
244 |
245 | if ((this.method === 'GET' || this.method === 'HEAD') && body) {
246 | throw new TypeError('Body not allowed for GET or HEAD requests')
247 | }
248 | this._initBody(body)
249 | }
250 |
251 | Request.prototype.clone = function() {
252 | return new Request(this)
253 | }
254 |
255 | function decode(body) {
256 | var form = new FormData()
257 | body.trim().split('&').forEach(function(bytes) {
258 | if (bytes) {
259 | var split = bytes.split('=')
260 | var name = split.shift().replace(/\+/g, ' ')
261 | var value = split.join('=').replace(/\+/g, ' ')
262 | form.append(decodeURIComponent(name), decodeURIComponent(value))
263 | }
264 | })
265 | return form
266 | }
267 |
268 | function headers(xhr) {
269 | var head = new Headers()
270 | var pairs = xhr.getAllResponseHeaders().trim().split('\n')
271 | pairs.forEach(function(header) {
272 | var split = header.trim().split(':')
273 | var key = split.shift().trim()
274 | var value = split.join(':').trim()
275 | head.append(key, value)
276 | })
277 | return head
278 | }
279 |
280 | Body.call(Request.prototype)
281 |
282 | function Response(bodyInit, options) {
283 | if (!options) {
284 | options = {}
285 | }
286 |
287 | this.type = 'default'
288 | this.status = options.status
289 | this.ok = this.status >= 200 && this.status < 300
290 | this.statusText = options.statusText
291 | this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers)
292 | this.url = options.url || ''
293 | this._initBody(bodyInit)
294 | }
295 |
296 | Body.call(Response.prototype)
297 |
298 | Response.prototype.clone = function() {
299 | return new Response(this._bodyInit, {
300 | status: this.status,
301 | statusText: this.statusText,
302 | headers: new Headers(this.headers),
303 | url: this.url
304 | })
305 | }
306 |
307 | Response.error = function() {
308 | var response = new Response(null, {status: 0, statusText: ''})
309 | response.type = 'error'
310 | return response
311 | }
312 |
313 | var redirectStatuses = [301, 302, 303, 307, 308]
314 |
315 | Response.redirect = function(url, status) {
316 | if (redirectStatuses.indexOf(status) === -1) {
317 | throw new RangeError('Invalid status code')
318 | }
319 |
320 | return new Response(null, {status: status, headers: {location: url}})
321 | }
322 |
323 | self.Headers = Headers;
324 | self.Request = Request;
325 | self.Response = Response;
326 |
327 | self.fetch = function(input, init) {
328 | return new Promise(function(resolve, reject) {
329 | var request
330 | if (Request.prototype.isPrototypeOf(input) && !init) {
331 | request = input
332 | } else {
333 | request = new Request(input, init)
334 | }
335 |
336 | var xhr = new XMLHttpRequest()
337 |
338 | function responseURL() {
339 | if ('responseURL' in xhr) {
340 | return xhr.responseURL
341 | }
342 |
343 | // Avoid security warnings on getResponseHeader when not allowed by CORS
344 | if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
345 | return xhr.getResponseHeader('X-Request-URL')
346 | }
347 |
348 | return;
349 | }
350 |
351 | xhr.onload = function() {
352 | var status = (xhr.status === 1223) ? 204 : xhr.status
353 | if (status < 100 || status > 599) {
354 | reject(new TypeError('Network request failed'))
355 | return
356 | }
357 | var options = {
358 | status: status,
359 | statusText: xhr.statusText,
360 | headers: headers(xhr),
361 | url: responseURL()
362 | }
363 | var body = 'response' in xhr ? xhr.response : xhr.responseText;
364 | resolve(new Response(body, options))
365 | }
366 |
367 | xhr.onerror = function() {
368 | reject(new TypeError('Network request failed'))
369 | }
370 |
371 | xhr.open(request.method, request.url, true)
372 |
373 | if (request.credentials === 'include') {
374 | xhr.withCredentials = true
375 | }
376 |
377 | if ('responseType' in xhr && support.blob) {
378 | xhr.responseType = 'blob'
379 | }
380 |
381 | request.headers.forEach(function(value, name) {
382 | xhr.setRequestHeader(name, value)
383 | })
384 |
385 | xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
386 | })
387 | }
388 | self.fetch.polyfill = true
389 | })(typeof self !== 'undefined' ? self : this);
390 |
--------------------------------------------------------------------------------
/static/styles/bootstrap.theme.scss:
--------------------------------------------------------------------------------
1 | .h1, .h2, .h3, h1, h2, h3 {
2 | margin-top: 20px;
3 | margin-bottom: 20px;
4 | }
5 |
6 | .badge {
7 | font-weight: normal;
8 | }
9 |
--------------------------------------------------------------------------------
/static/styles/hint.min.css:
--------------------------------------------------------------------------------
1 | /*! Hint.css - v2.1.0 - 2016-02-15
2 | * http://kushagragour.in/lab/hint/
3 | * Copyright (c) 2016 Kushagra Gour; Licensed */
4 |
5 | .hint--bottom-left:before,.hint--bottom-right:before,.hint--bottom:before{margin-top:-12px}[data-hint]{position:relative;display:inline-block}[data-hint]:after,[data-hint]:before{position:absolute;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;opacity:0;z-index:1000000;pointer-events:none;-webkit-transition:.3s ease;-moz-transition:.3s ease;transition:.3s ease;-webkit-transition-delay:0ms;-moz-transition-delay:0ms;transition-delay:0ms}[data-hint]:hover:after,[data-hint]:hover:before{visibility:visible;opacity:1;-webkit-transition-delay:100ms;-moz-transition-delay:100ms;transition-delay:100ms}[data-hint]:before{content:'';position:absolute;background:0 0;border:6px solid transparent;z-index:1000001}[data-hint]:after{content:attr(data-hint);background:#383838;color:#fff;padding:8px 10px;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;line-height:12px;white-space:nowrap;text-shadow:0 -1px 0 #000;box-shadow:4px 4px 8px rgba(0,0,0,.3)}[data-hint='']:after,[data-hint='']:before{display:none!important}.hint--top-left:before,.hint--top-right:before,.hint--top:before{border-top-color:#383838}.hint--bottom-left:before,.hint--bottom-right:before,.hint--bottom:before{border-bottom-color:#383838}.hint--top:before{margin-bottom:-12px}.hint--top:after,.hint--top:before{bottom:100%;left:50%;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--top:focus:after,.hint--top:focus:before,.hint--top:hover:after,.hint--top:hover:before{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--bottom:after,.hint--bottom:before{top:100%;left:50%;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--bottom:focus:after,.hint--bottom:focus:before,.hint--bottom:hover:after,.hint--bottom:hover:before{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--right:before{border-right-color:#383838;margin-left:-12px;margin-bottom:-6px}.hint--right:after{margin-bottom:-14px}.hint--right:after,.hint--right:before{left:100%;bottom:50%}.hint--right:focus:after,.hint--right:focus:before,.hint--right:hover:after,.hint--right:hover:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--left:before{border-left-color:#383838;margin-right:-12px;margin-bottom:-6px}.hint--left:after{margin-bottom:-14px}.hint--top-left:before,.hint--top-right:before{margin-bottom:-12px}.hint--left:after,.hint--left:before{right:100%;bottom:50%}.hint--left:focus:after,.hint--left:focus:before,.hint--left:hover:after,.hint--left:hover:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--top-left:after,.hint--top-left:before{bottom:100%;left:50%;-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%)}.hint--top-left:after{margin-left:6px}.hint--top-left:focus:after,.hint--top-left:focus:before,.hint--top-left:hover:after,.hint--top-left:hover:before{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--top-right:after,.hint--top-right:before{bottom:100%;left:50%;-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0)}.hint--top-right:after{margin-left:-6px}.hint--top-right:focus:after,.hint--top-right:focus:before,.hint--top-right:hover:after,.hint--top-right:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--bottom-left:after,.hint--bottom-left:before{top:100%;left:50%;-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%)}.hint--bottom-left:after{margin-left:6px}.hint--bottom-left:focus:after,.hint--bottom-left:focus:before,.hint--bottom-left:hover:after,.hint--bottom-left:hover:before{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--bottom-right:after,.hint--bottom-right:before{top:100%;left:50%;-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0)}.hint--bottom-right:after{margin-left:-6px}.hint--bottom-right:focus:after,.hint--bottom-right:focus:before,.hint--bottom-right:hover:after,.hint--bottom-right:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--error:after{background-color:#b34e4d;text-shadow:0 -1px 0 #592726}.hint--error.hint--top-left:before,.hint--error.hint--top-right:before,.hint--error.hint--top:before{border-top-color:#b34e4d}.hint--error.hint--bottom-left:before,.hint--error.hint--bottom-right:before,.hint--error.hint--bottom:before{border-bottom-color:#b34e4d}.hint--error.hint--left:before{border-left-color:#b34e4d}.hint--error.hint--right:before{border-right-color:#b34e4d}.hint--warning:after{background-color:#c09854;text-shadow:0 -1px 0 #6c5328}.hint--warning.hint--top-left:before,.hint--warning.hint--top-right:before,.hint--warning.hint--top:before{border-top-color:#c09854}.hint--warning.hint--bottom-left:before,.hint--warning.hint--bottom-right:before,.hint--warning.hint--bottom:before{border-bottom-color:#c09854}.hint--warning.hint--left:before{border-left-color:#c09854}.hint--warning.hint--right:before{border-right-color:#c09854}.hint--info:after{background-color:#3986ac;text-shadow:0 -1px 0 #1a3c4d}.hint--info.hint--top-left:before,.hint--info.hint--top-right:before,.hint--info.hint--top:before{border-top-color:#3986ac}.hint--info.hint--bottom-left:before,.hint--info.hint--bottom-right:before,.hint--info.hint--bottom:before{border-bottom-color:#3986ac}.hint--info.hint--left:before{border-left-color:#3986ac}.hint--info.hint--right:before{border-right-color:#3986ac}.hint--success:after{background-color:#458746;text-shadow:0 -1px 0 #1a321a}.hint--success.hint--top-left:before,.hint--success.hint--top-right:before,.hint--success.hint--top:before{border-top-color:#458746}.hint--success.hint--bottom-left:before,.hint--success.hint--bottom-right:before,.hint--success.hint--bottom:before{border-bottom-color:#458746}.hint--success.hint--left:before{border-left-color:#458746}.hint--success.hint--right:before{border-right-color:#458746}.hint--always:after,.hint--always:before{opacity:1;visibility:visible}.hint--always.hint--top:after,.hint--always.hint--top:before{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--always.hint--top-left:after,.hint--always.hint--top-left:before{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--always.hint--top-right:after,.hint--always.hint--top-right:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--bottom:after,.hint--always.hint--bottom:before{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--always.hint--bottom-left:after,.hint--always.hint--bottom-left:before{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--always.hint--bottom-right:after,.hint--always.hint--bottom-right:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--left:after,.hint--always.hint--left:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--always.hint--right:after,.hint--always.hint--right:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--rounded:after{border-radius:4px}.hint--no-animate:after,.hint--no-animate:before{-webkit-transition-duration:0ms;-moz-transition-duration:0ms;transition-duration:0ms}.hint--bounce:after,.hint--bounce:before{-webkit-transition:opacity .3s ease,visibility .3s ease,-webkit-transform .3s cubic-bezier(.71,1.7,.77,1.24);-moz-transition:opacity .3s ease,visibility .3s ease,-moz-transform .3s cubic-bezier(.71,1.7,.77,1.24);transition:opacity .3s ease,visibility .3s ease,transform .3s cubic-bezier(.71,1.7,.77,1.24)}
--------------------------------------------------------------------------------
/static/styles/react-modal.css:
--------------------------------------------------------------------------------
1 | .ReactModal__Overlay {
2 | -webkit-perspective: 600;
3 | perspective: 600;
4 | opacity: 0;
5 | overflow-x: hidden;
6 | overflow-y: auto;
7 | background-color: rgba(0, 0, 0, 0.5);
8 | }
9 |
10 | .ReactModal__Overlay--after-open {
11 | opacity: 1;
12 | transition: opacity 150ms ease-out;
13 | }
14 |
15 | .ReactModal__Content {
16 | -webkit-transform: scale(0.5) rotateX(-30deg);
17 | }
18 |
19 | .ReactModal__Content--after-open {
20 | -webkit-transform: scale(1) rotateX(0deg);
21 | transition: all 150ms ease-in;
22 | }
23 |
24 | .ReactModal__Overlay--before-close {
25 | opacity: 0;
26 | }
27 |
28 | .ReactModal__Content--before-close {
29 | -webkit-transform: scale(0.5) rotateX(30deg);
30 | transition: all 150ms ease-in;
31 | }
32 |
33 | .ReactModal__Content.modal-dialog {
34 | border: none;
35 | background-color: transparent;
36 | }
--------------------------------------------------------------------------------
/store/index.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./store.prod')
3 | } else {
4 | module.exports = require('./store.dev')
5 | }
6 |
--------------------------------------------------------------------------------
/store/store.dev.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { browserHistory } from 'react-router'
4 | import createLogger from 'redux-logger'
5 | import { routerMiddleware } from 'react-router-redux'
6 | import reducer from '../reducers'
7 | import DevTools from '../containers/DevTools'
8 |
9 | const reduxRouterMiddleware = routerMiddleware(browserHistory)
10 | const store = createStore(reducer, {}, compose(
11 | applyMiddleware(thunk, reduxRouterMiddleware, createLogger()),
12 | DevTools.instrument()
13 | ));
14 |
15 | // Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
16 | if (module.hot) {
17 | module.hot.accept('../reducers', () =>
18 | store.replaceReducer(require('../reducers')/*.default if you use Babel 6+ */)
19 | );
20 | }
21 |
22 | export default store
23 |
--------------------------------------------------------------------------------
/store/store.prod.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { browserHistory } from 'react-router'
4 | import { routerMiddleware } from 'react-router-redux'
5 | import reducer from '../reducers'
6 |
7 | const reduxRouterMiddleware = routerMiddleware(browserHistory)
8 | const store = createStore(reducer, {}, applyMiddleware(thunk, reduxRouterMiddleware));
9 |
10 | export default store
11 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | import AV from 'avoscloud-sdk'
2 |
3 | export function createReducer(initialState, handlers) {
4 | return (state = initialState, action) => {
5 | return handlers[action.type]
6 | ? handlers[action.type](state, action)
7 | : state
8 | }
9 | }
10 |
11 | export function initLeanCloud(appId = localStorage.getItem('appId'), appKey = localStorage.getItem('appKey'), appMasterKey = localStorage.getItem('appMasterKey')) {
12 | AV.initialize(appId, appKey);
13 | AV.masterKey = appMasterKey
14 | AV._useMasterKey = true
15 | }
16 |
17 | export function deinitLeanCloud() {
18 | AV.applicationId = null
19 | AV.applicationKey = null
20 | AV.masterKey = null
21 | AV._useMasterKey = false
22 | }
23 |
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'cheap-module-eval-source-map',
6 | entry: [
7 | 'webpack-hot-middleware/client',
8 | './index.js'
9 | ],
10 | output: {
11 | path: path.join(__dirname, 'webpack-output'),
12 | filename: 'bundle.js',
13 | publicPath: '/webpack-output/'
14 | },
15 | plugins: [
16 | new webpack.optimize.OccurenceOrderPlugin(),
17 | new webpack.HotModuleReplacementPlugin(),
18 | new webpack.NoErrorsPlugin()
19 | ],
20 | module: {
21 | loaders: [
22 | {
23 | test: /.js$/,
24 | loader: 'babel',
25 | exclude: /node_modules/,
26 | include: __dirname
27 | },
28 | {
29 | test: /\.css$/,
30 | loaders: ["style", "css"]
31 | },
32 | {
33 | test: /\.scss$/,
34 | loaders: ["style", "css", "sass"]
35 | }
36 | ]
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 |
4 | module.exports = {
5 | devtool: 'source-map',
6 | entry: [
7 | './index.js'
8 | ],
9 | output: {
10 | path: path.join(__dirname, 'webpack-output'),
11 | filename: 'bundle.js',
12 | publicPath: '/webpack-output/'
13 | },
14 | plugins: [
15 | new webpack.optimize.UglifyJsPlugin({
16 | compress: {
17 | warnings: false
18 | }
19 | }),
20 | new webpack.optimize.DedupePlugin(),
21 | new webpack.optimize.OccurenceOrderPlugin(),
22 | new webpack.DefinePlugin({
23 | 'process.env.NODE_ENV': JSON.stringify('production')
24 | }),
25 | ],
26 | module: {
27 | loaders: [
28 | {
29 | test: /.js$/,
30 | loader: 'babel',
31 | exclude: /node_modules/,
32 | include: __dirname
33 | },
34 | {
35 | test: /\.css$/,
36 | loaders: ["style", "css"]
37 | },
38 | {
39 | test: /\.scss$/,
40 | loaders: ["style", "css", "sass"]
41 | }
42 | ]
43 | },
44 | };
45 |
--------------------------------------------------------------------------------