├── .babelrc
├── .eslintrc
├── .gitignore
├── README.md
├── build
├── index.html
└── index.js
├── gulpfile.babel.js
├── index.html
├── package.json
├── src
├── components
│ ├── app.js
│ ├── blank.js
│ ├── common
│ │ ├── chat.js
│ │ ├── clip-button.js
│ │ ├── codeEditor.js
│ │ ├── login.js
│ │ ├── nameList.js
│ │ └── party.js
│ ├── owner
│ │ ├── create-party.js
│ │ └── party.js
│ ├── ownerApp.js
│ ├── utils
│ │ └── login.js
│ ├── watcher
│ │ ├── name.js
│ │ └── party.js
│ └── watcherApp.js
├── index.css
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-0"],
3 | "plugins": ["transform-runtime","transform-decorators-legacy"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": [
4 | "react"
5 | ],
6 | "env": {
7 | "browser": true,
8 | "node": false,
9 | "es6": true
10 | },
11 | "globals": {
12 | "Wilddog": true
13 | },
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "rules": {
18 | // Strict mode
19 | "strict": [2, "never"],
20 | "no-undef": [2],
21 | // Code style
22 | //http://guide.taobao.net/3.javascript-style-guide.html#anchor-h3-0
23 | "indent": [2, 2, {"SwitchCase": 1}],
24 | // http://guide.taobao.net/3.javascript-style-guide.html#anchor-h3-1
25 | "quotes": [2, "single"],
26 | "jsx-quotes": [2, "prefer-double"],
27 |
28 | // React
29 | "react/display-name": 0,
30 | "react/jsx-boolean-value": 0,
31 | "react/jsx-no-undef": 1,
32 | "react/jsx-sort-prop-types": 0,
33 | "react/jsx-sort-props": 0,
34 | "react/jsx-uses-react": 1,
35 | "react/jsx-uses-vars": 1,
36 | "react/no-did-mount-set-state": 1,
37 | "react/no-did-update-set-state": 1,
38 | "react/no-multi-comp": 1,
39 | "react/no-unknown-property": 1,
40 | "react/prop-types": 1,
41 | "react/react-in-jsx-scope": 1,
42 | "react/self-closing-comp": 1,
43 | "react/sort-comp": 1,
44 | "react/wrap-multilines": 1
45 | },
46 | "extends": ["eslint:recommended", "plugin:react/recommended"]
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules/
3 | secret.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 代码白板
2 |
3 | 实时的「代码白板」,可聊天。远程面试、远程解答问题的得力助手。
4 |
5 | [地址](https://xieguanglei.github.io/code-whiteboard/build/index.html)。
6 |
7 | [墙内地址](http://witcher.oss-cn-hangzhou.aliyuncs.com/code-whiteboard/index.html)。
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Code Whiteboard
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import gUtil from 'gulp-util';
3 | import webpack from 'webpack';
4 | import WebpackDevServer from 'webpack-dev-server';
5 | import genConfig from './webpack.config.js';
6 | import publish from 'gulp-oss-publish';
7 | import secret from './secret.json';
8 |
9 | const PORT = 2016;
10 | const dist = 'build';
11 | const env = {
12 | publicPath: `http://127.0.0.1:${PORT}/`
13 | };
14 |
15 |
16 | gulp.task('publish', () =>
17 | gulp
18 | .src('build/**/*', {
19 | base: 'build',
20 | buffer: true
21 | })
22 | .pipe(publish({
23 | prefix: 'code-whiteboard',
24 | genShortId: false,
25 | oss: {
26 | accessKeyId: secret.ossAcessKeyId,
27 | secretAccessKey: secret.ossAcessKey,
28 | endpoint: 'http://oss-cn-hangzhou.aliyuncs.com',
29 | bucket: 'witcher'
30 | },
31 | headers: {
32 | CacheControl: 'no-cache',
33 | ServerSideEncryption: 'AES256'
34 | }
35 | }))
36 | );
37 |
38 | gulp.task('build', (cb) => {
39 | webpack(genConfig({
40 | dist: dist,
41 | publicPath: env.publicPath,
42 | isDev: false
43 | }), function(err, stats) {
44 | if(err) throw new gUtil.PluginError('webpack', err);
45 | gUtil.log('[webpack]', stats.toString());
46 | cb();
47 | });
48 | });
49 |
50 | gulp.task('dev', () => {
51 |
52 | var config = genConfig({
53 | isDev: true,
54 | publicPath: env.publicPath,
55 | dist: dist
56 | });
57 |
58 | var compiler = webpack(config);
59 |
60 | var server = new WebpackDevServer(compiler, {
61 | contentBase: __dirname,
62 | publicPath: env.publicPath,
63 | hot: false,
64 | noInfo: false,
65 | headers: {
66 | 'Access-Control-Allow-Origin': '*',
67 | 'Access-Control-Allow-Methods': '*',
68 | 'Access-Control-Allow-Headers': '*'
69 | },
70 | stats: {
71 | colors: true
72 | }
73 | });
74 |
75 | server.listen(PORT, (err) => {
76 | if(err) throw new gUtil.PluginError('webpack-dev-server', err);
77 | gUtil.log('[webpack-dev-server]', env.publicPath);
78 | });
79 |
80 | });
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Code Whiteboard
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code-whiteboard",
3 | "version": "0.0.0",
4 | "description": "",
5 | "author": "xieguanglei@github",
6 | "devDependencies": {
7 | "babel-core": "^6.10.4",
8 | "babel-eslint": "^6.1.0",
9 | "babel-loader": "^6.2.4",
10 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
11 | "babel-plugin-transform-runtime": "^6.9.0",
12 | "babel-polyfill": "^6.9.1",
13 | "babel-preset-es2015": "^6.9.0",
14 | "babel-preset-react": "^6.11.1",
15 | "babel-preset-stage-0": "^6.5.0",
16 | "babel-runtime": "^6.9.2",
17 | "brace": "^0.8.0",
18 | "clipboard": "^1.5.12",
19 | "css-loader": "^0.23.1",
20 | "eslint": "^3.0.1",
21 | "eslint-loader": "^1.4.1",
22 | "eslint-plugin-react": "^5.2.2",
23 | "gulp": "^3.9.1",
24 | "gulp-oss-publish": "^1.0.4",
25 | "gulp-util": "^3.0.7",
26 | "json-loader": "^0.5.4",
27 | "lodash": "^4.13.1",
28 | "path": "^0.12.7",
29 | "raw-loader": "^0.5.1",
30 | "react": "0.14.8",
31 | "react-ace": "^3.4.1",
32 | "react-bootstrap": "0.29.3",
33 | "react-dom": "0.14.8",
34 | "style-loader": "^0.13.1",
35 | "url": "^0.11.0",
36 | "webpack": "^1.13.1",
37 | "webpack-dev-server": "^1.14.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/app.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import _ from 'lodash';
3 | import url from 'url';
4 |
5 | import OwnerApp from './ownerApp';
6 | import WatcherApp from './watcherApp';
7 |
8 | function getClientSize() {
9 | return {
10 | width: document.documentElement.clientWidth,
11 | height: document.documentElement.clientHeight
12 | }
13 | }
14 |
15 | const WATCHER = 1;
16 | const OWNER = 2;
17 | const token = url.parse(location.href, true).query.party;
18 | const role = token ? WATCHER : OWNER;
19 |
20 | class App extends Component {
21 |
22 | constructor(props) {
23 | super(props);
24 | this.state = {
25 | clientSize: getClientSize()
26 | }
27 | }
28 |
29 | componentDidMount() {
30 | this.sizeChangeListener = ()=> {
31 | this.setState({
32 | clientSize: getClientSize()
33 | })
34 | };
35 | window.addEventListener('resize', this.sizeChangeListener);
36 | }
37 |
38 | componentWillUnmount() {
39 | window.removeEventListener('resize', this.sizeChangeListener);
40 | }
41 |
42 | getStyles() {
43 |
44 | let {clientSize: {width, height}} = this.state;
45 |
46 | return {
47 | container: {
48 | width: _.min([width, 1000]),
49 | height: height,
50 | marginLeft: 'auto',
51 | marginRight: 'auto',
52 | position: 'relative'
53 | }
54 | }
55 | }
56 |
57 | render() {
58 |
59 | let styles = this.getStyles();
60 |
61 | let {clientSize} = this.state;
62 |
63 | return (
64 |
65 | {
66 | role === OWNER ?
67 | :
68 |
69 | }
70 |
71 | )
72 | }
73 | }
74 |
75 | App.propTypes = {
76 | someProp: PropTypes.array
77 | };
78 |
79 | export default App;
80 |
--------------------------------------------------------------------------------
/src/components/blank.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | class Blank extends Component {
4 | render() {
5 | return (
6 | Blank
7 | )
8 | }
9 | }
10 |
11 | Blank.propTypes = {
12 | someProp: PropTypes.array
13 | };
14 |
15 | export default Blank;
16 |
--------------------------------------------------------------------------------
/src/components/common/chat.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import {Form, FormGroup, FormControl, ControlLabel, Button, Col} from 'react-bootstrap';
4 |
5 | import _ from 'lodash';
6 |
7 | class Chat extends Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | message: ''
13 | }
14 | }
15 |
16 | doChangeMessage(message) {
17 | this.setState({message})
18 | }
19 |
20 | doHandleKeyPress(e) {
21 | if(e.charCode == 13) {
22 | e.preventDefault();
23 | this.doSendMessage();
24 | }
25 | }
26 |
27 | doSendMessage() {
28 | let {onMessage} = this.props;
29 | let {message} = this.state;
30 | if(message){
31 | onMessage(message);
32 | this.setState({
33 | message: ''
34 | })
35 | }
36 | }
37 |
38 | getStyles() {
39 |
40 | const {clientSize} = this.props;
41 |
42 | return {
43 |
44 | wrapper: {
45 | position: 'absolute',
46 | bottom: 10
47 | },
48 |
49 | messages: {
50 | height: clientSize.height - 150,
51 | marginBottom: 20,
52 | padding: 15,
53 | backgroundColor: '#ecf0f1',
54 | position: 'relative'
55 | },
56 |
57 | message: {
58 | display: 'block',
59 | clear: 'both',
60 | marginBottom: 5
61 | },
62 |
63 | submitButton: {
64 | marginTop: -9
65 | }
66 | }
67 | }
68 |
69 | render() {
70 |
71 | const styles = this.getStyles();
72 |
73 | let {messages} = this.props;
74 |
75 | let {message} = this.state;
76 |
77 | return (
78 |
79 |
80 |
81 | {
82 | _.map(messages, (m, i)=>
83 |
84 | {`${m.name} 说:${m.message}`}
85 |
86 | )
87 | }
88 |
89 |
90 |
91 |
106 |
107 |
108 | )
109 | }
110 | }
111 |
112 | Chat.propTypes = {
113 | messages: PropTypes.array,
114 | onMessage: PropTypes.func,
115 | clientSize: PropTypes.object
116 | };
117 |
118 | export default Chat;
119 |
--------------------------------------------------------------------------------
/src/components/common/clip-button.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import {Button} from 'react-bootstrap';
3 |
4 | import Clipboard from 'clipboard';
5 |
6 | class ClipButton extends Component {
7 |
8 | constructor(props){
9 | super(props);
10 | this.state = {
11 | justCopied: false
12 | }
13 | }
14 |
15 | componentDidMount(){
16 | let c = new Clipboard('#clip-url-button');
17 |
18 | c.on('success', e => {
19 | this.setState({justCopied: true});
20 |
21 | setTimeout(()=>{
22 | this.setState({justCopied: false});
23 | }, 2000);
24 |
25 | e.clearSelection();
26 | });
27 | }
28 |
29 | render() {
30 | let {value} = this.props;
31 | let {justCopied} = this.state;
32 |
33 | return (
34 |
37 | )
38 | }
39 | }
40 |
41 | ClipButton.propTypes = {
42 | value: PropTypes.string
43 | };
44 |
45 | export default ClipButton;
46 |
--------------------------------------------------------------------------------
/src/components/common/codeEditor.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import AceEditor from 'react-ace';
4 |
5 | import 'brace';
6 | import 'brace/mode/javascript';
7 | import 'brace/theme/github';
8 |
9 |
10 | const styles = {
11 | container: {
12 | border: '1px solid #aaa',
13 | height: '100%'
14 | }
15 | };
16 |
17 |
18 | class CodeEditor extends Component {
19 |
20 | render() {
21 |
22 | let {codeText, onChange} = this.props;
23 |
24 | return (
25 |
36 | )
37 | }
38 | }
39 |
40 | CodeEditor.propTypes = {
41 | codeText: PropTypes.string,
42 | onChange: PropTypes.func
43 | };
44 |
45 | export default CodeEditor;
46 |
--------------------------------------------------------------------------------
/src/components/common/login.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | class Login extends Component {
4 |
5 | render() {
6 | return (
7 |
29 | )
30 | }
31 | }
32 |
33 | Login.propTypes = {
34 | someProp: PropTypes.array
35 | };
36 |
37 | export default Login;
38 |
--------------------------------------------------------------------------------
/src/components/common/nameList.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import _ from 'lodash';
4 |
5 | class NameList extends Component {
6 |
7 | getStyles(){
8 |
9 | const name = {
10 | float: 'left',
11 | color: '#27ae60',
12 | backgroundColor: '#ecf0f1',
13 | margin: 10,
14 | borderRadius: 5,
15 | padding: 5
16 | };
17 |
18 | return {
19 | ownerName: {
20 | ...name
21 | },
22 |
23 | userName: {
24 | ...name
25 | }
26 | }
27 | }
28 |
29 |
30 | render() {
31 |
32 | let styles = this.getStyles();
33 |
34 | let {users, ownerName} = this.props;
35 |
36 | return (
37 |
38 |
{ownerName}
39 | {_.map(users, (u, k)=>
40 |
{u}
41 | )}
42 |
43 | )
44 | }
45 | }
46 |
47 | NameList.propTypes = {
48 | users: PropTypes.object,
49 | ownerName: PropTypes.string
50 | };
51 |
52 | export default NameList;
53 |
--------------------------------------------------------------------------------
/src/components/common/party.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | const NAME_LIST = 1;
4 | const CHAT_AREA = 2;
5 |
6 |
7 | class Party extends Component {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | currentTab: CHAT_AREA
13 | }
14 | }
15 |
16 | doChangeTab(currentTab) {
17 | this.setState({currentTab})
18 | }
19 |
20 | getStyles() {
21 |
22 | const {clientSize} = this.props;
23 | const {currentTab} = this.state;
24 |
25 | const headHeight = 30;
26 | const padding = 5;
27 | const mainHeight = clientSize.height - headHeight;
28 | const packupHeight = 40;
29 |
30 | return {
31 | top: {
32 | height: headHeight,
33 | lineHeight: headHeight + 'px',
34 | paddingLeft: 5
35 | },
36 | main: {
37 | height: mainHeight,
38 | overflow: 'hidden'
39 | },
40 | codeEditor: {
41 | marginRight: 405,
42 | height: mainHeight - 12,
43 | padding: padding
44 | },
45 | rightCol: {
46 | float: 'right',
47 | width: 400,
48 | height: '100%',
49 | padding: 5
50 | },
51 | nameList: {
52 | height: currentTab === NAME_LIST ? mainHeight - packupHeight - padding * 4 : packupHeight,
53 | overflow: 'hidden'
54 | },
55 | chat: {
56 | height: currentTab === CHAT_AREA ? mainHeight - packupHeight - padding * 4 : packupHeight,
57 | overflow: 'hidden'
58 | },
59 |
60 | tabButton: {
61 | height: packupHeight - padding * 2,
62 | lineHeight: packupHeight - padding * 2 + 'px',
63 | backgroundColor: '#3d566e',
64 | color: '#ecf0f1',
65 | textAlign: 'center',
66 | borderRadius: packupHeight * 0.3,
67 | cursor: 'pointer'
68 | }
69 | }
70 | }
71 |
72 | render() {
73 |
74 | let styles = this.getStyles();
75 |
76 | let {children} = this.props;
77 | const {currentTab} = this.state;
78 |
79 | return (
80 |
81 |
82 | {children[3]}
83 |
84 |
85 |
86 |
87 | {
88 | currentTab === NAME_LIST ? children[1] :
89 |
this.doChangeTab(NAME_LIST)} style={styles.tabButton}>展开名单
90 | }
91 |
92 |
93 | {
94 | currentTab === CHAT_AREA ? children[2] :
95 |
this.doChangeTab(CHAT_AREA)} style={styles.tabButton}>聊天
96 | }
97 |
98 |
99 |
100 | {children[0]}
101 |
102 |
103 |
104 | )
105 | }
106 | }
107 |
108 | Party.propTypes = {
109 | children: PropTypes.node,
110 | token: PropTypes.string,
111 | clientSize: PropTypes.object
112 | };
113 |
114 | export default Party;
115 |
--------------------------------------------------------------------------------
/src/components/owner/create-party.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import {Form, FormGroup, FormControl, ControlLabel, Button, Col} from 'react-bootstrap';
4 |
5 | class CreateParty extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | partyName: '',
10 | ownerName: ''
11 | }
12 | }
13 |
14 | doChange(key, val) {
15 | let source = {};
16 | source[key] = val;
17 | this.setState(source);
18 | }
19 |
20 | doSubmit() {
21 | let {onSubmit} = this.props;
22 | onSubmit(this.state);
23 | }
24 |
25 | getStyles() {
26 | return {
27 | container: {
28 | position: 'absolute',
29 | width: 400,
30 | height: 300,
31 | top: '50%',
32 | left: '50%',
33 | marginLeft: -200,
34 | marginTop: -150
35 | },
36 | title: {
37 | marginBottom: 40
38 | },
39 | submitButton: {
40 | float: 'right',
41 | marginRight: 20
42 | }
43 | }
44 | }
45 |
46 | render() {
47 |
48 | let styles = this.getStyles();
49 |
50 | let {partyName, ownerName} = this.state;
51 |
52 | return (
53 |
54 |
创建派对
55 |
79 |
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 | CreateParty.propTypes = {
87 | onSubmit: PropTypes.func
88 | };
89 |
90 | export default CreateParty;
91 |
--------------------------------------------------------------------------------
/src/components/owner/party.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xieguanglei/code-whiteboard/7e01be0f125a9f791c2f1b881cafd3d9d55a8417/src/components/owner/party.js
--------------------------------------------------------------------------------
/src/components/ownerApp.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 | import _ from 'lodash';
3 |
4 | import Login from './common/login';
5 | import CreateParty from './owner/create-party';
6 | import Party from './common/party';
7 | import CodeEditor from './common/codeEditor';
8 | import NameList from './common/nameList';
9 | import Chat from './common/chat';
10 | import ClipButton from './common/clip-button';
11 | import login from './utils/login';
12 |
13 | let STAGE_LOGIN = 1;
14 | let STAGE_CREATEPARTY = 2;
15 | let STAGE_PARTY = 3;
16 |
17 | class OwnerApp extends Component {
18 | constructor(props) {
19 | super(props);
20 | this.state = {
21 | stage: STAGE_LOGIN
22 | }
23 | }
24 |
25 | componentDidMount() {
26 |
27 | this.wdRef = login((err)=>{
28 | if(err) {
29 | alert('Fail to login');
30 | } else {
31 | this.setState({
32 | stage: STAGE_CREATEPARTY
33 | });
34 | //setTimeout(()=>{
35 | // this.doCreateParty({partyName: 'party', ownerName: 'name'});
36 | //}, 500)
37 | }
38 | });
39 | }
40 |
41 |
42 | doCreateParty(party) {
43 |
44 | party = _.pick(party, ['partyName', 'ownerName']);
45 | party.codeText = '';
46 | party.users = {};
47 | party.messages = [];
48 |
49 | this.partyRef = this.wdRef.push(party, ()=> {
50 | this.setState({
51 | stage: STAGE_PARTY
52 | })
53 | });
54 |
55 | this.partyRef.on('value', s=> {
56 | this.setState(s.val())
57 | });
58 |
59 | this.partyRef.onDisconnect().remove();
60 |
61 | }
62 |
63 | doChangeCode(codeText) {
64 | this.setState({
65 | codeText
66 | }, ()=> {
67 | this.partyRef.child('codeText').set(codeText);
68 | });
69 | }
70 |
71 | doSubmitMessage(message) {
72 | let {ownerName: name, messages=[]} = this.state;
73 | this.partyRef.child('messages').set([...messages, {name, message}]);
74 | }
75 |
76 | render() {
77 |
78 | let {clientSize} = this.props;
79 | let {stage, codeText, users, ownerName, messages} = this.state;
80 |
81 | switch (stage) {
82 | case STAGE_LOGIN:
83 | return ;
84 | case STAGE_CREATEPARTY:
85 | return this.doCreateParty(party)}/>;
86 | case STAGE_PARTY:
87 | return (
88 |
89 | this.doChangeCode(t)}/>
90 |
91 | this.doSubmitMessage(message)}/>
92 | 点击 发给你的朋友们,请他们也来参加派对!
93 |
94 | );
95 | default:
96 | return None
;
97 | }
98 | }
99 | }
100 |
101 | OwnerApp.propTypes = {
102 | clientSize: PropTypes.object
103 | };
104 |
105 | export default OwnerApp;
106 |
--------------------------------------------------------------------------------
/src/components/utils/login.js:
--------------------------------------------------------------------------------
1 | function login(callback){
2 | let wdRef = new Wilddog('https://witcher3.wilddogio.com');
3 |
4 | wdRef.authAnonymously((err, auth)=> {
5 |
6 | setTimeout(()=>{
7 | callback(err, auth);
8 | }, 10);
9 | });
10 |
11 | return wdRef;
12 | }
13 |
14 | export default login;
--------------------------------------------------------------------------------
/src/components/watcher/name.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import {Form, FormGroup, FormControl, ControlLabel, Button, Col} from 'react-bootstrap';
4 |
5 | class Name extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | name: ''
10 | }
11 | }
12 |
13 | doChange(key, val) {
14 | let source = {};
15 | source[key] = val;
16 | this.setState(source);
17 | }
18 |
19 | doSubmit() {
20 | let {onSubmit} = this.props;
21 | onSubmit(this.state);
22 | }
23 |
24 | getStyles() {
25 | return {
26 | container: {
27 | position: 'absolute',
28 | width: 400,
29 | height: 300,
30 | top: '50%',
31 | left: '50%',
32 | marginLeft: -200,
33 | marginTop: -150
34 | },
35 | title: {
36 | marginBottom: 40
37 | },
38 | submitButton: {
39 | float: 'right',
40 | marginRight: 20
41 | }
42 | }
43 | }
44 |
45 | render() {
46 |
47 | let {name} = this.state;
48 |
49 | let styles = this.getStyles();
50 |
51 | return (
52 |
53 |
输入名字
54 |
67 |
68 |
69 |
70 | )
71 | }
72 | }
73 |
74 | Name.propTypes = {
75 | onSubmit: PropTypes.func
76 | };
77 |
78 | export default Name;
79 |
--------------------------------------------------------------------------------
/src/components/watcher/party.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | class Party extends Component {
4 |
5 | getStyles() {
6 |
7 | let {clientSize} = this.props;
8 |
9 | const headHeight = 30;
10 | const nameListHeight = 50;
11 |
12 | return {
13 |
14 | container: {
15 | overflow: 'hidden'
16 | },
17 |
18 |
19 | top: {
20 | height: headHeight,
21 | lineHeight: headHeight + 'px',
22 | paddingLeft: 5
23 | },
24 | main: {
25 | height: clientSize.height - headHeight
26 | },
27 | codeEditor: {
28 | marginRight: 405,
29 | height: '100%'
30 | },
31 | rightCol: {
32 | float: 'right',
33 | width: 400,
34 | height: '100%'
35 | },
36 | nameList:{
37 | height: nameListHeight
38 | },
39 | chat:{
40 | height: clientSize.height - headHeight - nameListHeight
41 | }
42 | }
43 |
44 | }
45 |
46 | render() {
47 |
48 | let styles = this.getStyles();
49 |
50 | let {children} = this.props;
51 |
52 | return (
53 |
54 |
55 | 你正在参加一场 party
56 |
57 |
58 |
59 |
{children[1]}
60 |
{children[2]}
61 |
62 |
63 | {children[0]}
64 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
71 | Party.propTypes = {
72 | children: PropTypes.node,
73 | clientSize: PropTypes.object
74 | };
75 |
76 | export default Party;
77 |
--------------------------------------------------------------------------------
/src/components/watcherApp.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes, Component} from 'react';
2 |
3 | import CodeEditor from './common/codeEditor';
4 | import Login from './common/login';
5 | import Party from './common/party';
6 | import Name from './watcher/name';
7 | import NameList from './common/nameList';
8 | import Chat from './common/chat';
9 |
10 | import login from './utils/login';
11 |
12 | const STAGE_LOGIN = 1;
13 | const STAGE_NAME = 2;
14 | const STAGE_PARTY = 3;
15 |
16 | class WatcherApp extends Component {
17 |
18 | constructor(props) {
19 | super(props);
20 | this.state = {
21 | stage: STAGE_LOGIN
22 | }
23 | }
24 |
25 | componentDidMount() {
26 |
27 | this.wdRef = login((err)=> {
28 | if(err) {
29 | alert('Fail to login');
30 | } else {
31 |
32 | this.setState({
33 | stage: STAGE_NAME
34 | });
35 |
36 | this.partyRef = this.wdRef.child(this.props.token);
37 | this.partyRef.on('value', s=> {
38 |
39 | this.setState(s.val());
40 | });
41 |
42 | }
43 | });
44 |
45 |
46 | this.wdRef.on('child_removed', s=> {
47 | let party = s.val();
48 | let {partyName: p1, ownerName: o1} = party;
49 | let {partyName: p2, ownerName: o2} = this.state;
50 | if(p1 === p2 && o1 == o2) {
51 | this.setState({closed: true})
52 | }
53 | })
54 | }
55 |
56 | doSubmitName(e){
57 |
58 | if(!this.state.closed) {
59 | this.userRef = this.partyRef.child('users').push(e.name, ()=> {
60 |
61 | this.setState({
62 | stage: STAGE_PARTY,
63 | name: e.name
64 | });
65 |
66 | this.userRef.onDisconnect().remove();
67 | });
68 | }
69 | }
70 |
71 | doChangeCode(codeText) {
72 |
73 | if(!this.state.closed) {
74 | this.setState({
75 | codeText
76 | }, ()=> {
77 | this.partyRef.child('codeText').set(codeText);
78 | })
79 | }
80 |
81 | }
82 |
83 | doSubmitMessage(message) {
84 |
85 | if(!this.state.closed) {
86 | let {name, messages=[]} = this.state;
87 | this.partyRef.child('messages').set([...messages, {name, message}]);
88 | }
89 |
90 | }
91 |
92 | render() {
93 |
94 | let {clientSize} = this.props;
95 |
96 | let {stage, codeText, users, ownerName, messages, closed} = this.state;
97 |
98 | switch (stage){
99 | case STAGE_LOGIN:
100 | return ;
101 | case STAGE_NAME:
102 | return this.doSubmitName(e)}/>;
103 | case STAGE_PARTY:
104 | return(
105 |
106 | this.doChangeCode(t)}/>
107 |
108 | this.doSubmitMessage(message)}/>
109 | {
110 | closed ? 举办者{ownerName}已离开,派对结束了。
:
111 | 你正在参加{ownerName}的派对。
112 | }
113 |
114 | );
115 | default:
116 | return None
117 | }
118 | }
119 | }
120 |
121 | WatcherApp.propTypes = {
122 | someProp: PropTypes.array,
123 | token: PropTypes.string,
124 | clientSize: PropTypes.object
125 | };
126 |
127 | export default WatcherApp;
128 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | .form-control{
2 | background-color: #FFFFFF;
3 | background-image: none;
4 | border: 1px solid #e5e6e7;
5 | border-radius: 1px;
6 | color: inherit;
7 | display: block;
8 | padding: 6px 12px;
9 | transition: border-color 0.15s ease-in-out 0s, box-shadow 0.15s ease-in-out 0s;
10 | width: 100%;
11 | font-size: 14px;
12 | }
13 | .form-control:focus{
14 | border-color: #1ab394 !important;
15 | }
16 |
17 |
18 |
19 | html body{
20 | font-family: 'PingFang SC','Microsoft YaHei','Heiti SC';
21 | }
22 |
23 |
24 |
25 | .spinner {
26 | margin: 300px auto;
27 | width: 60px;
28 | height: 60px;
29 | position: relative;
30 | }
31 |
32 | .container1 > div, .container2 > div, .container3 > div {
33 | width: 18px;
34 | height: 18px;
35 | background-color: #333;
36 |
37 | border-radius: 100%;
38 | position: absolute;
39 | -webkit-animation: bouncedelay 1.2s infinite ease-in-out;
40 | animation: bouncedelay 1.2s infinite ease-in-out;
41 | -webkit-animation-fill-mode: both;
42 | animation-fill-mode: both;
43 | }
44 |
45 | .spinner .spinner-container {
46 | position: absolute;
47 | width: 100%;
48 | height: 100%;
49 | }
50 |
51 | .container2 {
52 | -webkit-transform: rotateZ(45deg);
53 | transform: rotateZ(45deg);
54 | }
55 |
56 | .container3 {
57 | -webkit-transform: rotateZ(90deg);
58 | transform: rotateZ(90deg);
59 | }
60 |
61 | .circle1 { top: 0; left: 0; }
62 | .circle2 { top: 0; right: 0; }
63 | .circle3 { right: 0; bottom: 0; }
64 | .circle4 { left: 0; bottom: 0; }
65 |
66 | .container2 .circle1 {
67 | -webkit-animation-delay: -1.1s;
68 | animation-delay: -1.1s;
69 | }
70 |
71 | .container3 .circle1 {
72 | -webkit-animation-delay: -1.0s;
73 | animation-delay: -1.0s;
74 | }
75 |
76 | .container1 .circle2 {
77 | -webkit-animation-delay: -0.9s;
78 | animation-delay: -0.9s;
79 | }
80 |
81 | .container2 .circle2 {
82 | -webkit-animation-delay: -0.8s;
83 | animation-delay: -0.8s;
84 | }
85 |
86 | .container3 .circle2 {
87 | -webkit-animation-delay: -0.7s;
88 | animation-delay: -0.7s;
89 | }
90 |
91 | .container1 .circle3 {
92 | -webkit-animation-delay: -0.6s;
93 | animation-delay: -0.6s;
94 | }
95 |
96 | .container2 .circle3 {
97 | -webkit-animation-delay: -0.5s;
98 | animation-delay: -0.5s;
99 | }
100 |
101 | .container3 .circle3 {
102 | -webkit-animation-delay: -0.4s;
103 | animation-delay: -0.4s;
104 | }
105 |
106 | .container1 .circle4 {
107 | -webkit-animation-delay: -0.3s;
108 | animation-delay: -0.3s;
109 | }
110 |
111 | .container2 .circle4 {
112 | -webkit-animation-delay: -0.2s;
113 | animation-delay: -0.2s;
114 | }
115 |
116 | .container3 .circle4 {
117 | -webkit-animation-delay: -0.1s;
118 | animation-delay: -0.1s;
119 | }
120 |
121 | @-webkit-keyframes bouncedelay {
122 | 0%, 80%, 100% { -webkit-transform: scale(0.0) }
123 | 40% { -webkit-transform: scale(1.0) }
124 | }
125 |
126 | @keyframes bouncedelay {
127 | 0%, 80%, 100% {
128 | transform: scale(0.0);
129 | -webkit-transform: scale(0.0);
130 | } 40% {
131 | transform: scale(1.0);
132 | -webkit-transform: scale(1.0);
133 | }
134 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import React from 'react';
4 | import {render} from 'react-dom';
5 |
6 | import AppElement from './components/app';
7 |
8 | import './index.css';
9 |
10 | render(
11 | ,
12 | document.querySelector('#container')
13 | );
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 |
4 | var path = require('path');
5 |
6 | module.exports = function (options) {
7 | options = options || {};
8 |
9 | var isDev = options.isDev;
10 |
11 | var plugins = [];
12 | if (!isDev) {
13 | plugins.push(new webpack.optimize.DedupePlugin());
14 | plugins.push(new webpack.optimize.UglifyJsPlugin({
15 | compressor: {
16 | warnings: false
17 | }
18 | }));
19 | }
20 |
21 | return {
22 | entry: {
23 | index: './src/index.js'
24 | },
25 | output: {
26 | path: path.join(__dirname, options.dist),
27 | filename: '[name].js',
28 | chunkFilename: '[id].chunk.js',
29 | publicPath: options.publicPath,
30 | sourceMapFilename: "debugging/[file].map"
31 | },
32 | module: {
33 | preLoaders: [
34 | {
35 | test: /\.js$/,
36 | exclude: /(node_modules|bower_components)/,
37 | loader: 'eslint-loader'
38 | }
39 | ],
40 | loaders: [
41 | {
42 | test: /\.js$/,
43 | exclude: /(node_modules|bower_components)/,
44 | loader: 'babel-loader'
45 | },
46 | {
47 | test: /\.txt$/,
48 | loader: 'raw-loader'
49 | },
50 | {
51 | test: /\.json/,
52 | loader: 'json-loader'
53 | },
54 | {
55 | test: /\.css/,
56 | loader: 'style-loader!css-loader'
57 | }
58 | ]
59 | },
60 |
61 | plugins: plugins
62 | };
63 | };
64 |
--------------------------------------------------------------------------------