From 141d64e36b8d383184c90d1a8a7e7ef639c77dcb Mon Sep 17 00:00:00 2001 From: Isabel Jenkins <unreturnable@sucs.org> Date: Tue, 18 Jul 2017 10:00:44 +0100 Subject: [PATCH] it now does chat --- index.html | 2 +- package-lock.json | 20 +++- package.json | 1 + src/App.vue | 26 ++++ src/assets/logo-orange.png | Bin 0 -> 17072 bytes src/components/chat.vue | 44 ++++++- src/components/login.vue | 126 ++++++++++++++++++++ src/components/online.vue | 83 +++++++++++-- src/components/rooms.vue | 2 +- src/components/talk.vue | 14 ++- src/components/topbar.vue | 25 +++- src/mixins/mw-command.js | 56 +++++++++ src/mixins/mw-sync.js | 216 ++++++++++++++++++++++++++++++++++ src/mixins/string-to-color.js | 19 +++ src/mixins/url-checker.js | 10 ++ src/pages/chat.vue | 28 +++-- src/pages/login.vue | 20 ---- src/router/index.js | 6 - 18 files changed, 638 insertions(+), 60 deletions(-) create mode 100644 src/assets/logo-orange.png create mode 100644 src/components/login.vue create mode 100644 src/mixins/mw-command.js create mode 100644 src/mixins/mw-sync.js create mode 100644 src/mixins/string-to-color.js create mode 100644 src/mixins/url-checker.js delete mode 100644 src/pages/login.vue diff --git a/index.html b/index.html index 6c854be..76e31ba 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ <meta charset="utf-8"> <link rel="shortcut icon" type="image/png" href="./static/favicon.png"/> <meta name="theme-color" content="#e65c00"> - <meta name="viewport" content="width=device-width, initial-scale=0.8"> + <meta name="viewport" content="width=0.7, initial-scale=0.7"> <title>Web Milliways</title> </head> <body> diff --git a/package-lock.json b/package-lock.json index 61352b0..3312d5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -321,6 +321,15 @@ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", "dev": true }, + "axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha1-uk+S8XFn37q0CYN4VFS5rBScPG0=", + "requires": { + "follow-redirects": "1.2.4", + "is-buffer": "1.1.5" + } + }, "babel-code-frame": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", @@ -3526,6 +3535,14 @@ "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", "dev": true }, + "follow-redirects": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.4.tgz", + "integrity": "sha512-Suw6KewLV2hReSyEOeql+UUkBVyiBm3ok1VPrVFRZnQInWpdoZbbiG5i8aJVSjTr0yQ4Ava0Sh6/joCg1Brdqw==", + "requires": { + "debug": "2.6.8" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -4401,8 +4418,7 @@ "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" }, "is-builtin-module": { "version": "1.0.0", diff --git a/package.json b/package.json index ca9564e..3bd06a3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { + "axios": "^0.16.2", "pouchdb-browser": "^6.3.2", "pouchdb-find": "^6.3.2", "pouchdb-live-find": "^0.4.0", diff --git a/src/App.vue b/src/App.vue index 964361b..bc4699e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -24,4 +24,30 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } + +.scroll { + overflow-y: hidden; +} + +@media only screen and (max-width: 800px) { + .scroll { + overflow-y: overlay; + } +} + +.scroll:hover { + overflow-y: overlay; +} + +.scroll::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.5); +} + +.scroll::-webkit-scrollbar { + width: 8px; +} + +.scroll::-webkit-scrollbar-thumb { + background-color: #000000; +} </style> diff --git a/src/assets/logo-orange.png b/src/assets/logo-orange.png new file mode 100644 index 0000000000000000000000000000000000000000..e7f4c58dfada980e6b3046c93e52a53d9d079af1 GIT binary patch literal 17072 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 C4AGAO literal 0 HcmV?d00001 diff --git a/src/components/chat.vue b/src/components/chat.vue index 73fc0dc..68c8e40 100644 --- a/src/components/chat.vue +++ b/src/components/chat.vue @@ -1,16 +1,25 @@ <template> - <div id="chat"> - <p>{{ msg }}</p> + <div id="chat" class="scroll"> + <div id="chat-inner"> + <div v-for="msg in talker"> + <div class="msg"> + {{ msg.photo }} + <p> + <b>{{ msg.user }}</b> {{ msg.timestamp }} + <br> + <span v-html="msg.msg"/> + </p> + </div> + </div> + </div> </div> </template> <script> export default { name: 'chat', - data () { - return { - msg: 'Test message' - } + pouch: { + talker: {} } } </script> @@ -18,7 +27,30 @@ export default { <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #chat { + height: calc(100vh - 110px); margin-left: 10px; margin-right: 10px; + margin-top: 10px; + overflow-x: hidden; +} + +#chat-inner { + margin-right: 5px; +} + +@media only screen and (max-width: 800px) { + #chat { + height: calc(100vh - 180px); + margin-left: 5px; + margin-right: 0px; + } + + #chat-inner { + margin-right: 10px; + } + + .msg { + font-size: 8pt; + } } </style> diff --git a/src/components/login.vue b/src/components/login.vue new file mode 100644 index 0000000..c2b67be --- /dev/null +++ b/src/components/login.vue @@ -0,0 +1,126 @@ +<template> + <div id="login"> + <div id="login-wrap"> + <img src="../assets/logo-orange.png" class="logo"> + <form id="login-form"> + <input type="text" id="login-user" placeholder="Username" autofocus/><br> + <input type="password" id="login-pass" placeholder="Password"/><br> + <input type="submit" id="login-submit" class="button" value="Login"> + </form> + </div> + </div> +</template> + +<script> +import PouchDB from 'pouchdb-browser' +import axios from 'axios' + +export default { + name: 'login', + data () { + return { + } + }, + mounted: function () { + var db = new PouchDB('auth') + + db.get('user') + .then(function (result) { + hideLogin() + return + }).catch(function () { + // Do nothing since we need to auth the user + }) + + document.getElementById('login-form').addEventListener('submit', function (e) { + var user = document.getElementById('login-user').value + var pass = document.getElementById('login-pass').value + + axios.post('https://sucs.org/~unreturnable/webmw-test/', { username: user, password: pass, action: 'login' }) + .then(function (response) { + if (typeof response.data === 'string') { + if (response.data.startsWith('success:')) { + var token = response.data.replace('success:', '') + + db.get('user') + .then(function (success) { + db.remove(success) + .then(function (success) { + db.put({ _id: 'user', token: token }) + .then(function (success) { + location.reload() + }).catch(function () { + }) + }).catch(function () { + }) + }).catch(function () { + db.put({ _id: 'user', token: token }) + .then(function (success) { + location.reload() + }).catch(function () { + }) + }) + } + } else { + console.log(response.data.error) + } + }).catch(function (error) { + console.log(error.message) + }) + + return false + }) + } +} + +function hideLogin () { + var login = document.getElementById('login') + login.parentElement.removeChild(login) +} +</script> + +<!-- Add "scoped" attribute to limit CSS to this component only --> +<style scoped> +#login { + position: absolute; + top: 0; + width: 100vw; + height: 100vh; + background-color: #f2f2f2; +} + +#login-wrap { + width: 100%; + text-align: center; + margin-top: 100px; +} + +#login-header { + font-size: 32pt; +} + +#login-user { + padding: 10px; +} + +#login-pass { + margin-top: 10px; + padding: 10px; +} + +#login-submit { + margin-top: 10px; +} + +.button { + background: #e65c00; + color: #f2f2f2; + padding: 10px 40px 10px 40px; + border: none; +} + +.logo { + width: 200px; + margin-left: -15px; +} +</style> diff --git a/src/components/online.vue b/src/components/online.vue index b3f3241..e21a5f2 100644 --- a/src/components/online.vue +++ b/src/components/online.vue @@ -1,15 +1,25 @@ <template> <div class="bar-wrapper" v-if="config"> - <div id="online" v-bind:style="{ 'background': config.onlineBg, 'color': config.onlineFg }"> + <div id="online" class="scroll" v-bind:style="{ 'background': config.onlineBg, 'color': config.onlineFg }"> <div id="online-list"> - <h2>Online</h2> - <h3>{{ user }}</h3> + <h2 id="online-header">Online</h2> + <div v-for="user in online"> + <div class="user"> + {{ user.name }} + <div v-html="user.photo"></div> + </div> + </div> </div> </div> </div> </template> <script> +import PouchDB from 'pouchdb-browser' +import mwCommand from '../mixins/mw-command.js' +import urlChecker from '../mixins/url-checker.js' +import stringToColor from '../mixins/string-to-color.js' + export default { name: 'rooms', pouch: { @@ -19,11 +29,51 @@ export default { selector: { _id: 'user' }, first: true } - } + }, + online: {} }, - data () { - return { - user: 'unreturnable' + mixins: [ mwCommand, urlChecker, stringToColor ], + created: function () { + var ctx = this + ctx.sendCmdCallback('who', ctx.syncWho) + setInterval(function () { ctx.sendCmdCallback('who', ctx.syncWho) }, 5000) + }, + methods: { + syncWho: function (response) { + if (response.state !== 'WHOL') { + var ctx = this + var db = new PouchDB('online') + + db.destroy() + .then(function (res) { + ctx.storeUsers(response.data) + }).catch(function (err) { + console.log(err) + }) + } + }, + storeUsers: function (users) { + var db = new PouchDB('online') + + for (var i = 0; i < users.length; i++) { + users[i]._id = users[i].name.toLowerCase() + // users[i].photo = this.getHackergotchi(users[i].name) + db.put(users[i]) + .then(function (result) { + }).catch(function () { + // This was likely caused by the same user logged in with two clients + }) + } + }, + getHackergotchi: function (username) { + var hackergotchi = 'https://sucs.org/pictures/people/' + username.toLowerCase() + '.png' + var profileImage = '' + if (this.urlExists(hackergotchi)) { + profileImage = '<img src="' + hackergotchi + '" width="36"/>' + } else { + profileImage = '<div style="width: 36px; height: 36px; font-size: 7pt; text-align:center;padding-top: 5px; background-color: ' + this.stringToColour(username) + '">Don\'t <br>Panic</div>' + } + return profileImage } } } @@ -69,10 +119,13 @@ function handleGesure () { height: 100vh; } +#online-header { + margin-right: 25px; +} + #online-list { text-align: right; - margin-top: 25px; - margin-right: 25px; + margin-top: 50px; } @media only screen and (max-width: 800px) { @@ -81,4 +134,16 @@ function handleGesure () { right: -250px; } } + +.user { + font-size: 12pt; + padding-top: 5px; + padding-bottom: 5px; + padding-right: 25px; +} + +.user:hover { + background: rgba(0, 0, 0, 0.5); + cursor: pointer; +} </style> diff --git a/src/components/rooms.vue b/src/components/rooms.vue index b97357a..0ee5f58 100644 --- a/src/components/rooms.vue +++ b/src/components/rooms.vue @@ -1,6 +1,6 @@ <template> <div class="bar-wrapper" v-if="config"> - <div id="rooms" v-bind:style="{ 'background': config.roomsBg, 'color': config.roomsFg }"> + <div id="rooms" class="scroll" v-bind:style="{ 'background': config.roomsBg, 'color': config.roomsFg }"> <img src="../assets/logo.png" class="logo"> <div id="rooms-list"> <h2>Rooms</h2> diff --git a/src/components/talk.vue b/src/components/talk.vue index b5156fc..265d0f2 100644 --- a/src/components/talk.vue +++ b/src/components/talk.vue @@ -1,13 +1,15 @@ <template> <div id="talk" v-if="config"> <textarea id="talk-bar" placeholder=""></textarea> - <button id="send" v-bind:style="{ 'background': config.buttonBg, 'color': config.buttonFg }"> + <button id="send" v-on:click="onSubmit" v-bind:style="{ 'background': config.buttonBg, 'color': config.buttonFg }"> Send </button> </div> </template> <script> +import mwCommand from '../mixins/mw-command.js' + export default { name: 'chat', pouch: { @@ -19,10 +21,12 @@ export default { } } }, - data () { - return { - bgColor: '#e65c00', - fgColor: '#f2f2f2' + mixins: [ mwCommand ], + methods: { + onSubmit: function () { + var text = document.getElementById('talk-bar').value + document.getElementById('talk-bar').value + this.sendSay(text) } } } diff --git a/src/components/topbar.vue b/src/components/topbar.vue index 187858f..adb1915 100644 --- a/src/components/topbar.vue +++ b/src/components/topbar.vue @@ -6,13 +6,15 @@ Settings </button> <button id="logout" class="button" v-bind:style="{ 'background': config.buttonBg, 'color': - config.buttonFg }"> + config.buttonFg }" v-on:click="logout"> Logout </button> </div> </template> <script> +import PouchDB from 'pouchdb-browser' + export default { name: 'topbar', pouch: { @@ -22,13 +24,32 @@ export default { selector: { _id: 'user' }, first: true } - } + }, + auth: {} }, data () { return { bgColor: '#e65c00', fgColor: '#f2f2f2' } + }, + methods: { + logout: function (event) { + var db = new PouchDB('auth') + + db.get('user') + .then(function (result) { + console.log(result) + db.remove(result) + .then(function (result) { + location.reload() + }).catch(function (err) { + console.log(err) + }) + }).catch(function () { + // Do nothing since we need to auth the user + }) + } } } </script> diff --git a/src/mixins/mw-command.js b/src/mixins/mw-command.js new file mode 100644 index 0000000..af04a98 --- /dev/null +++ b/src/mixins/mw-command.js @@ -0,0 +1,56 @@ +import axios from 'axios' +import PouchDB from 'pouchdb-browser' + +export default { + created: function () { + }, + methods: { + sendCmdCallback: function (cmd, callback, err) { + var db = new PouchDB('auth') + + db.get('user') + .then(function (result) { + axios.post('https://sucs.org/~unreturnable/webmw-test/send.php', { send: cmd, mwsess: result.token }) + .then(function (response) { + callback(response) + }).catch(function (error) { + console.log(error) + }) + }).catch(function (err) { + console.log(err) + }) + }, + sendCmd: function (cmd) { + var db = new PouchDB('auth') + + db.get('user') + .then(function (result) { + axios.post('https://sucs.org/~unreturnable/webmw-test/send.php', { send: cmd, mwsess: result.token }) + .then(function (response) { + // Do nothing because we didn't want + }).catch(function (error) { + console.log(error) + }) + }).catch(function (err) { + console.log(err) + }) + }, + sendSay: function (text) { + var db = new PouchDB('auth') + var what = 'say ' + text + + db.get('user') + .then(function (result) { + axios.post('https://sucs.org/~unreturnable/webmw-test/send.php', { send: what, mwsess: result.token }) + .then(function (response) { + }).catch(function (error) { + console.log(error) + }) + }).catch(function (err) { + console.log(err) + }) + + return false + } + } +} diff --git a/src/mixins/mw-sync.js b/src/mixins/mw-sync.js new file mode 100644 index 0000000..6bb6c76 --- /dev/null +++ b/src/mixins/mw-sync.js @@ -0,0 +1,216 @@ +import PouchDB from 'pouchdb-browser' +import axios from 'axios' +import urlChecker from '../mixins/url-checker.js' + +var profileImages = {} + +export default { + created: function () { + var mixin = this + var db = new PouchDB('auth') + + db.get('user') + .then(function (result) { + setInterval(function () { mixin.sync(result.token) }, 1000) + }).catch(function (err) { + console.log(err) + }) + }, + mixins: [ urlChecker ], + methods: { + sync: function (token) { + var mixin = this + + axios.get('https://sucs.org/~unreturnable/webmw-test/poll.php', { params: { mwsess: token } }) + .then(mixin.onMsg).catch(function (error) { + console.log(error) + }) + }, + filterString: function (str) { + var escapedStr = str.replace(/&/g, '&') + escapedStr = escapedStr.replace(/</g, '<') + escapedStr = escapedStr.replace(/>/g, '>') + escapedStr = escapedStr.replace(/\n/g, '') + escapedStr = escapedStr.replace(/\r/g, '') + + return escapedStr + }, + filterColours: function (str) { + /* Replace colour codes with appropriate span tags */ + var msgColours = str.match(/\u001b../g) + if (msgColours !== null) { + for (var i = 0; i < msgColours.length; i++) { + if (isNaN(msgColours[i].charAt(1))) { + var colourBold = '' + var fgColour = msgColours[i].charAt(1) + if (fgColour !== fgColour.toLowerCase()) { + colourBold = ' bold' + } + str = str.replace(msgColours[i], '</span><span class=\'colourfg' + msgColours[i].charAt(1) + colourBold + ' colourbg' + msgColours[i].charAt(2) + '\'>') + } else { + str = str.replace(msgColours[i], '</span><span class=\'colour' + msgColours[i].replace(/\u001b/g, '') + '\'>') + } + } + } + return str + }, + pad: function (number, length) { + var str = '' + number + while (str.length < length) { + str = '0' + str + } + return str + }, + getHackergotchi: function (username) { + if (typeof profileImages[username] === 'undefined') { + var hackergotchi = 'https://sucs.org/pictures/people/' + username.toLowerCase() + '.png' + if (this.urlExists(hackergotchi)) { + profileImages[username] = '<img src="' + hackergotchi + '" width="36" />' + } else { + profileImages[username] = '<div style="width: 36px; height: 36px; font-size: 7pt; text-align: center; padding-top: 5px; background-color: ' + this.stringToColour(username) + '">Don\'t <br>Panic</div>' + } + } + + return profileImages[username] + }, + onMsg: function (msg) { + console.log(msg) + + var db = new PouchDB('talker') + + if (msg.status !== 200) { + if (msg.status === 'Socket open error') { + console.log('Error connecting to socket') + } else { + console.log('Problem: ' + msg.status) + } + } else { + msg = msg.data + + for (var one in msg) { + switch (msg[one].state) { + case 'SAYR': + case 'SAYU': + // decode the json message + var detail = JSON.parse(msg[one].text) + + var body = detail.text + // make a crude approximation of the old message styles + switch (detail.type) { + case 'say': + body = detail.text + break + case 'raw': + body = detail.text + break + case 'emote': + switch (detail.plural) { + case 1: + body = msg[one].name + '\'s ' + detail.text + break + case 2: + body = msg[one].name + '\' ' + detail.text + break + case 3: + body = msg[one].name + '\'d ' + detail.text + break + case 4: + body = msg[one].name + '\'ll ' + detail.text + break + default: + body = msg[one].name + ' ' + detail.text + break + } + break + case 'notsayto': + body = msg[one].name + ' says (-' + detail.exclude + '): ' + detail.text + break + case 'says': + body = msg[one].name + ' says: ' + detail.text + break + case 'whispers': + body = msg[one].name + ' whispers: ' + detail.text + break + default: + body = msg[one].name + ' ' + detail.text + break + } + + /* Escape HTML characters */ + var escapedMsg = this.filterString(body) + escapedMsg = this.filterColours(escapedMsg) + + var msgTime = new Date(msg[one].when * 1000) + var timestamp = this.pad(msgTime.getHours(), 2) + ':' + this.pad(msgTime.getMinutes(), 2) + + var toStore = { + _id: msg[one].serial.toString(), + user: msg[one].name, + photo: this.getHackergotchi(msg[one].name), + timestamp: timestamp, + msg: escapedMsg + } + + db.put(toStore) + .then(function (result) { + }).catch(function () { + // Messages currently can't get updated in MW + }) + break + + case 11: // Talker message + case 17: // Board Message + /* Escape HTML characters */ + escapedMsg = this.filterString(msg[one].text) + escapedMsg = this.filterColours(escapedMsg) + + msgTime = new Date(msg[one].when * 1000) + timestamp = this.pad(msgTime.getHours(), 2) + ':' + this.pad(msgTime.getMinutes(), 2) + + toStore = { + id_: timestamp, + timestamp: timestamp, + msg: escapedMsg + } + + db.put(toStore) + .then(function (result) { + console.log(result) + }).catch(function () { + // Messages currently can't get updated in MW + }) + break + + case 14: // IPC_KICK + var what = '<div class=\'msgkick user_system\'>' + if (msg[one].text.substr(0, 1) === 'm') { + what += 'Boing, Zebedee\'s arrived. "Look up!" says Zebedee<br />' + what += 'You look up; a large object is falling towards you very fast, very very fast. It looks like a Magic Roundabout!<br />' + what += '"I wouldn\'t stand there if I was you" says Zebedee<br />' + what += 'WWWHHHEEEEEEEKKKKEEEERRRRRUUUUUNNNNNCCCCCHHHHHH<br />' + what += msg[one].name + ' has just dropped the Magic Roundabout of Death on you.<br/>' + } else { + what += 'Boing, Zebedee arrived.<br />' + what += '"Time for bed" says Zebedee<br />' + what += msg[one].name + ' has just dropped the Zebedee of Death on you.<br />' + } + if (msg[one].text.substr(1, 1) === 'r') what += '"' + msg[one].text.substr(2) + '" says Zebedee<br />' + what += '</div>' + + console.log(what) + break + + case 23: // CheckOnOff + // Implement who check + break + + default: + console.log('Unknown message type \'' + msg[one].state + '\'') + console.log(msg[one].text) + break + } + } + } + } + } +} diff --git a/src/mixins/string-to-color.js b/src/mixins/string-to-color.js new file mode 100644 index 0000000..68764cf --- /dev/null +++ b/src/mixins/string-to-color.js @@ -0,0 +1,19 @@ +export default { + methods: { + stringToColour: function (str) { + var hash = 0 + + for (var i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash) + } + + var colour = '#' + for (i = 0; i < 3; i++) { + var value = (hash >> (i * 8)) & 0xFF + colour += ('00' + value.toString(16)).substr(-2) + } + + return colour + } + } +} diff --git a/src/mixins/url-checker.js b/src/mixins/url-checker.js new file mode 100644 index 0000000..d13520c --- /dev/null +++ b/src/mixins/url-checker.js @@ -0,0 +1,10 @@ +export default { + methods: { + urlExists: function (url) { + var http = new XMLHttpRequest() + http.open('HEAD', url, false) + http.send() + return http.status !== 404 + } + } +} diff --git a/src/pages/chat.vue b/src/pages/chat.vue index 6bc87aa..f0f9ace 100644 --- a/src/pages/chat.vue +++ b/src/pages/chat.vue @@ -1,12 +1,15 @@ <template> - <div id="container"> - <rooms></rooms> - <div id="center-container"> - <topbar></topbar> - <chat></chat> - <talk></talk> + <div> + <div id="container"> + <rooms></rooms> + <div id="center-container"> + <topbar></topbar> + <chat></chat> + <talk></talk> + </div> + <online></online> </div> - <online></online> + <login></login> </div> </template> @@ -16,6 +19,10 @@ import topbar from '../components/topbar.vue' import chat from '../components/chat.vue' import talk from '../components/talk.vue' import online from '../components/online.vue' +import login from '../components/login.vue' + +import mwSync from '../mixins/mw-sync.js' +import mwCommand from '../mixins/mw-command.js' export default { components: { @@ -23,7 +30,12 @@ export default { 'topbar': topbar, 'chat': chat, 'talk': talk, - 'online': online + 'online': online, + 'login': login + }, + mixins: [ mwSync, mwCommand ], + created: function () { + this.sendCmd('replay count 999') } } </script> diff --git a/src/pages/login.vue b/src/pages/login.vue deleted file mode 100644 index abf65f7..0000000 --- a/src/pages/login.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> - <div class="login"> - <h1>{{ msg }}</h1> - </div> -</template> - -<script> -export default { - name: 'login', - data () { - return { - msg: 'Login' - } - } -} -</script> - -<!-- Add "scoped" attribute to limit CSS to this component only --> -<style scoped> -</style> diff --git a/src/router/index.js b/src/router/index.js index 9a9b198..f92cfae 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,7 +1,6 @@ import Vue from 'vue' import Router from 'vue-router' import chat from '@/pages/chat' -import login from '@/pages/login' Vue.use(Router) @@ -11,11 +10,6 @@ export default new Router({ path: '/', name: 'Chat', component: chat - }, - { - path: '/login', - name: 'Login', - component: login } ] }) -- GitLab