├── .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 | Fork me on GitHub 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 | Fork me on GitHub 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 |
92 | 93 | 94 | 95 | this.doChangeMessage(e.target.value)} 98 | onKeyPress={e=>this.doHandleKeyPress(e)} 99 | /> 100 | 101 | 102 | 103 | 104 | 105 |
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 |
26 | 35 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
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 |
56 | 57 | 58 | 59 | 派对的名字 60 | 61 | 62 | this.doChange('partyName', e.target.value)}/> 65 | 66 | 67 | 68 | 69 | 70 | 你的名字 71 | 72 | 73 | this.doChange('ownerName', e.target.value)}/> 76 | 77 | 78 |
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 |
55 | 56 | 57 | 58 | 你的名字 59 | 60 | 61 | this.doChange('name', e.target.value)}/> 64 | 65 | 66 |
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 | --------------------------------------------------------------------------------