Initial Commit
9
.eslintrc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"plugins": ["@vue"],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
21
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw*
|
||||
5
babel.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
11260
package-lock.json
generated
Normal file
59
package.json
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "chatlol",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/eslint-plugin": "^4.2.0",
|
||||
"axios": "^0.18.0",
|
||||
"jquery": "^3.3.1",
|
||||
"socket.io": "^2.2.0",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"uws": "^10.148.1",
|
||||
"vue": "^2.5.17",
|
||||
"vue-headful": "^2.0.1",
|
||||
"vue-mq": "^1.0.1",
|
||||
"vue-recaptcha": "^1.1.1",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-socket.io": "^3.0.4",
|
||||
"vue-socket.io-extended": "^3.2.0",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.0.5",
|
||||
"@vue/cli-plugin-eslint": "^3.0.5",
|
||||
"@vue/cli-service": "^3.0.5",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0-0",
|
||||
"vue-template-compiler": "^2.5.17"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
]
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
30
public/index.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<title>Nertivia</title>
|
||||
<!-- Google recaptcha -->
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=vueRecaptchaApiLoaded&render=explicit" async defer>
|
||||
</script>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-131765299-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'UA-131765299-1');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but Nertivia doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
5
src/Main.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
BIN
src/assets/LogoAnimation/map.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src/assets/LogoAnimation/message.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/background.jpg
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
src/assets/graphics/HomeGraphics.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/assets/loading.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
2
src/assets/spinner.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
16
src/assets/status/0.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse stroke="#919191" ry="171" rx="171" id="svg_1" cy="199" cx="201" stroke-width="33" fill="none"/>
|
||||
<line stroke="#919191" transform="rotate(45 200.0000000000001,199.99999999999997) " stroke-linecap="null" stroke-linejoin="null" id="svg_12" y2="310.075198" x2="199.999992" y1="89.924784" x1="199.999992" fill-opacity="null" stroke-opacity="null" stroke-width="33" fill="none"/>
|
||||
<line stroke="#919191" transform="rotate(-45 199.99999999999991,199.99999999999994) " stroke-linecap="null" stroke-linejoin="null" id="svg_13" y2="310.075198" x2="199.999992" y1="89.924784" x1="199.999992" fill-opacity="null" stroke-opacity="null" stroke-width="33" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
src/assets/status/1.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse stroke="#26ff4a" ry="171" rx="171" id="svg_1" cy="198.69697" cx="200.69697" stroke-width="33" fill="none"/>
|
||||
<line stroke="#26ff4a" transform="rotate(45 225.0000000000001,212.29728698730466) " stroke-linecap="null" stroke-linejoin="null" id="svg_13" y2="320.251181" x2="224.999985" y1="104.343413" x1="224.999985" fill-opacity="null" stroke-width="33" fill="none"/>
|
||||
<line stroke="#26ff4a" transform="rotate(-45 130.6943359374999,247.77433776855463) " stroke-linecap="null" stroke-linejoin="null" id="svg_14" y2="281.545712" x2="130.694293" y1="213.423268" x1="130.694293" fill-opacity="null" stroke-width="33" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
16
src/assets/status/2.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#ffdd1e" fill="none"/>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="105" id="svg_2" y="279.4375" x="140.5" stroke-width="0" stroke="#ffdd1e" fill="#ffdd1e">Z</text>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="105" id="svg_3" y="193.4375" x="210.5" stroke-width="0" stroke="#ffdd1e" fill="#ffdd1e">Z</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1,009 B |
15
src/assets/status/3.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#ea0b1e" fill="none"/>
|
||||
<rect stroke="#ea0b1e" id="svg_4" height="6" width="60.999999" y="197" x="169.5" stroke-width="33" fill="#ea0b1e"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 727 B |
15
src/assets/status/4.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Created with Method Draw - http://github.com/duopixel/Method-Draw/ -->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="402" width="402" y="-1" x="-1"/>
|
||||
<g display="none" overflow="visible" y="0" x="0" height="100%" width="100%" id="canvasGrid">
|
||||
<rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<ellipse ry="171" rx="171" id="svg_1" cy="199" cx="200" stroke-width="33" stroke="#9a3dd3" fill="none"/>
|
||||
<text xml:space="preserve" text-anchor="start" font-family="Helvetica, Arial, sans-serif" font-size="227" id="svg_3" y="278.5" x="168.46875" stroke-width="0" stroke="#ffdd1e" fill="#9a3dd3">!</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
97
src/changelog.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const config = [
|
||||
{
|
||||
title: 'Avatar',
|
||||
shortTitle: 'Avatar',
|
||||
date: '29/01/2019',
|
||||
new: [
|
||||
'Settings page has been added.',
|
||||
'You can now set your own profile picture from the settings page.',
|
||||
'You can now logout within the app.'
|
||||
],
|
||||
next: [
|
||||
'Typing status.',
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Message limit, Chat message changes',
|
||||
shortTitle: 'Message limit',
|
||||
date: '25/01/2019',
|
||||
new: [
|
||||
'Changed the design of messages slightly and changed the font size.'
|
||||
],
|
||||
fix: [
|
||||
'Messages now have a limit of 5000 characters.',
|
||||
],
|
||||
next: [
|
||||
'I have decided to add profile pictures in the next update.',
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: 'ReeeeCaptcha :D',
|
||||
shortTitle: 'ReCaptcha',
|
||||
date: '23/01/2019',
|
||||
new: [
|
||||
'Added reCaptcha to our login and register so our website is safe from any spam accounts that could be created by bots.',
|
||||
],
|
||||
next: [
|
||||
'Typing status or maybe profile pictures (haven\'t decided yet)',
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
title: 'Online status and fixes',
|
||||
shortTitle: 'Online status and fixes',
|
||||
date: '22/01/2019',
|
||||
new: [
|
||||
'See if your friends are online, away, busy, looking to play or offline.',
|
||||
'Planned features and the latest change now shows in app.'
|
||||
],
|
||||
fix: [
|
||||
'Messages will no longer show twice when sending.',
|
||||
'Adjusted padding on some places.',
|
||||
'Message font is now much smaller.'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
title: 'Send and receive messages (experimental)',
|
||||
shortTitle: 'Send and receive messages',
|
||||
date: '14/01/2019',
|
||||
new: [
|
||||
'You can now send messages to your friends!',
|
||||
],
|
||||
next: [
|
||||
'improving the new messaging functionality and adding typing indicators, online statuses.',
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Public change log, Accept friends',
|
||||
shortTitle: 'Public change log, Accept friends',
|
||||
date: '09/01/2019',
|
||||
new: [
|
||||
'Added a change log so you can see how much progress is being made to Nertivia.',
|
||||
'Adding friends, denying requests, accepting requests is now possible.'
|
||||
],
|
||||
next: [
|
||||
'Ability to send messages.',
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Issues fixed',
|
||||
shortTitle: 'Issues fixed',
|
||||
date: '04/01/2019',
|
||||
msg: 'Tweaks have been made to the website here and there.'
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Compatibility',
|
||||
date: '31/12/2018',
|
||||
msg: 'Website is now compatible for viewing on mobile, tablet and desktop devices.'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export default config;
|
||||
18
src/clickOutside.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
// to close popout menus when clicking outside.
|
||||
Vue.directive('click-outside', {
|
||||
bind: function (el, binding, vnode) {
|
||||
el.clickOutsideEvent = function (event) {
|
||||
// here I check that click was outside the el and his childrens
|
||||
if (!(el == event.target || el.contains(event.target))) {
|
||||
// and if it did, call method provided in attribute value
|
||||
vnode.context[binding.expression](event);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
unbind: function (el) {
|
||||
document.body.removeEventListener('click', el.clickOutsideEvent)
|
||||
},
|
||||
});
|
||||
47
src/components/Button.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<button type="submit" :disabled="$props.loading">
|
||||
<div class="loading-icon" v-if="$props.loading" ></div>
|
||||
<div class="text">{{$props.message}}</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
"loading",
|
||||
"message"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
display: flex;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(0, 0, 0, 0.212);
|
||||
padding: 10px;
|
||||
color: white;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
transition: background-color 0.3s;
|
||||
height: 40px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: rgba(0, 0, 0, 0.541);
|
||||
}
|
||||
button:focus {
|
||||
background-color: rgba(0, 0, 0, 0.541);
|
||||
}
|
||||
.loading-icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-size: 100%;
|
||||
background-image: url(../assets/spinner.svg);
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
154
src/components/ChangeLog.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<div class="change-log">
|
||||
<div class="inner">
|
||||
<div class="close-button" @click="close">
|
||||
<i class="material-icons">
|
||||
close
|
||||
</i>
|
||||
</div>
|
||||
<div class="change-title">Change Log <div class="changelog-icon"><i class="material-icons ">update</i></div></div>
|
||||
<div class="change-list">
|
||||
|
||||
<div class="change" v-for="change in changelog" :key="change.title">
|
||||
<div class="date">{{change.date}}</div>
|
||||
<div class="changes-title">{{change.title}}</div>
|
||||
<div class="information">
|
||||
<div v-if="change.new">
|
||||
<strong>What's new?</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wnew, index) in change.new" :key="index">{{wnew}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="change.fix">
|
||||
<strong>Issues fixed</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wfix, index) in change.fix" :key="index">{{wfix}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="change.next">
|
||||
<strong>Up next</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wnext, index) in change.next" :key="index">{{wnext}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="change.msg">
|
||||
{{change.msg}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from './../main.js'
|
||||
import changelog from '@/changelog.js'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
changelog
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
bus.$emit('closeChangeLog')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.change-log{
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.342);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
color: white;
|
||||
}
|
||||
.inner{
|
||||
background-color: rgba(0, 0, 0, 0.664);
|
||||
margin: auto;
|
||||
width: 600px;
|
||||
height: 700px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.close-button{
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
color: grey;
|
||||
transition: .3s;
|
||||
}
|
||||
.close-button:hover{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.close-button .material-icons {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.change-title{
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
padding-bottom: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.changelog-icon{
|
||||
margin-top: 2px;
|
||||
margin-left: 10px
|
||||
}
|
||||
.changelog-icon .material-icons {
|
||||
font-size: 40px;
|
||||
}
|
||||
.change-list {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
.change{
|
||||
margin: 5px;
|
||||
min-height: 100px;
|
||||
border-bottom: solid 1px white;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.change:last-child{
|
||||
border-bottom: none;
|
||||
}
|
||||
.date{
|
||||
text-align: right;
|
||||
padding-top: 10px;
|
||||
padding-right: 10px;
|
||||
color: grey;
|
||||
}
|
||||
.changes-title {
|
||||
font-size: 25px;
|
||||
padding-left: 15px;
|
||||
color: rgba(255, 255, 255, 0.795);
|
||||
}
|
||||
.information {
|
||||
margin: 10px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
.inner {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
25
src/components/Recaptcha.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<vue-recaptcha ref="recaptcha" :sitekey="sitekey" theme="dark" @verify="submit"></vue-recaptcha>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueRecaptcha from 'vue-recaptcha';
|
||||
import config from '@/config.js'
|
||||
export default {
|
||||
components: { VueRecaptcha },
|
||||
data () {
|
||||
return {
|
||||
sitekey: config.recaptcha,
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit(response) {
|
||||
this.$emit('verify', response)
|
||||
},
|
||||
resetRecaptcha () {
|
||||
this.$refs.recaptcha.reset() // Direct call reset method
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
36
src/components/Spinner.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="loading-screen">
|
||||
<div class="loading-animation"></div>
|
||||
<div class="title">{{$props.msg}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
"msg"
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.loading-screen{
|
||||
margin: 50px;
|
||||
}
|
||||
.loading-animation{
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
background-size: 100%;
|
||||
background-image: url(../assets/spinner.svg);
|
||||
display: table;
|
||||
margin: auto;
|
||||
}
|
||||
.title {
|
||||
display: table;
|
||||
margin: auto;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
100
src/components/app/ConnectingScreen.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div class="connecting-screen">
|
||||
<div class="center-box">
|
||||
<div class="animation">
|
||||
<div class="map"></div>
|
||||
<div class="flash-message"></div>
|
||||
</div>
|
||||
<div class="message">Connecting...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.connecting-screen {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.76);
|
||||
color: white;
|
||||
}
|
||||
.center-box{
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.animation{
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
background-color: #2CB4FF;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.map {
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
background-position: -490px center;
|
||||
background-size: 170%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(./../../assets/LogoAnimation/map.png);
|
||||
animation: rotateGlobe;
|
||||
animation-timing-function: linear;
|
||||
animation-duration: 4s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
}
|
||||
|
||||
@keyframes rotateGlobe {
|
||||
from {
|
||||
background-position: -490px center;
|
||||
}
|
||||
to {
|
||||
background-position: 300px center;
|
||||
}
|
||||
}
|
||||
|
||||
.flash-message {
|
||||
height: 300px;
|
||||
width: 300px;
|
||||
margin-top: -300px;
|
||||
background-position: center;
|
||||
background-size: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(./../../assets/LogoAnimation/message.png);
|
||||
animation: flashMessage;
|
||||
animation-timing-function: linear;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes flashMessage {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
background-size: 55%;
|
||||
}
|
||||
30% {
|
||||
background-size: 50%;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message{
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 20px;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
63
src/components/app/LeftPanel.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="left-panel">
|
||||
<MyMiniInformation />
|
||||
<div class="list">
|
||||
<PendingFriends />
|
||||
<Friends />
|
||||
</div>
|
||||
<AddFriendPanel/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import MyMiniInformation from '../../components/app/MyMiniInformation.vue'
|
||||
import PendingFriends from './relationships/PendingFriends.vue'
|
||||
import AddFriendPanel from './relationships/AddFriendPanel.vue'
|
||||
import Friends from './relationships/Friends.vue'
|
||||
export default {
|
||||
components: {
|
||||
MyMiniInformation,
|
||||
PendingFriends,
|
||||
AddFriendPanel,
|
||||
Friends
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
.left-panel {
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.671);
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column
|
||||
}
|
||||
.list{
|
||||
margin: 10px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ------- SCROLL BAR -------*/
|
||||
/* width */
|
||||
.list::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.list::-webkit-scrollbar-track {
|
||||
background: #8080806b;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
.list::-webkit-scrollbar-thumb {
|
||||
background: #f5f5f559;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
.list::-webkit-scrollbar-thumb:hover {
|
||||
background: #f5f5f59e;
|
||||
}
|
||||
</style>
|
||||
135
src/components/app/MessageTemplate.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
<div class="message">
|
||||
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
|
||||
<div class="triangle">
|
||||
<div class="triangle-inner"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="username">{{this.$props.username}}</div>
|
||||
<div class="content-message">{{this.$props.message}}</div>
|
||||
</div>
|
||||
<div class="sending-status">{{statusMessage}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import config from '@/config.js'
|
||||
export default {
|
||||
props: ['message', 'status', 'username', 'avatar'],
|
||||
data() {
|
||||
return{
|
||||
userAvatar: config.domain + "/avatars/" + this.$props.avatar
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusMessage(){
|
||||
let status = this.$props.status;
|
||||
|
||||
if (status == 0) {
|
||||
return "Sending"
|
||||
} else if (status == 1) {
|
||||
return "Sent"
|
||||
} else if (status == 2) {
|
||||
return "Failed"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.message{
|
||||
margin: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
|
||||
animation: showMessage .3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes showMessage {
|
||||
from {
|
||||
transform: translate(0px, 9px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-picture{
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-color: rgba(0, 0, 0, 0.281);
|
||||
margin: auto;
|
||||
margin-bottom: 0;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
margin-left: 0;
|
||||
flex-shrink: 0;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.triangle{
|
||||
display: flex;
|
||||
justify-content: bottom;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
margin-left: 0;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 8.7px;
|
||||
}
|
||||
.triangle-inner{
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 7px solid transparent;
|
||||
border-right: 7px solid rgba(0, 0, 0, 0.301);
|
||||
}
|
||||
|
||||
.content{
|
||||
background: rgba(0, 0, 0, 0.301);
|
||||
padding: 10px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
color: rgb(231, 231, 231);
|
||||
margin: auto;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
transition: 1s;
|
||||
}
|
||||
.username {
|
||||
color: rgb(189, 189, 189);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-message {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message .sending-status {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
padding-bottom: 5px;
|
||||
margin-left: 10px;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
align-self: normal;
|
||||
user-select: none;
|
||||
transition: 0.5;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
187
src/components/app/MyMiniInformation.vue
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
<template>
|
||||
<div class="my-mini-information">
|
||||
<div class="profile-picture" :style="`background-image: url(${avatar})`"></div>
|
||||
<div class="information">
|
||||
<div class="name">{{user.username}}</div>
|
||||
<div class="tag">@{{user.tag}}</div>
|
||||
|
||||
<div class="status" v-on:click="status.isPoppedOut = !status.isPoppedOut">
|
||||
<img class="current-status" :src="getStatus"/>
|
||||
<i class="material-icons expand-status-icon">expand_more</i>
|
||||
<transition name="show-status-list">
|
||||
<statusList v-if="status.isPoppedOut" v-click-outside="closeMenus" class="status-popout" />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-icon" @click="openSettings">
|
||||
<i class="material-icons">settings</i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../../main';
|
||||
import config from '@/config.js'
|
||||
import statusList from '../../components/app/statusList.vue'
|
||||
import settingsService from '@/services/settingsService'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
statusList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: {
|
||||
isPoppedOut: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
closeMenus() {
|
||||
this.status.isPoppedOut = false;
|
||||
},
|
||||
async changeStatus (status){
|
||||
// emit to server to change their status.
|
||||
console.log(status)
|
||||
const {ok, error, result} = await settingsService.setStatus(status);
|
||||
if (ok && result.data.status == true) {
|
||||
this.$store.dispatch('changeStatus', result.data.set)
|
||||
}
|
||||
},
|
||||
openSettings() {
|
||||
bus.$emit('openSettings');
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//When user changes their own status (statusList.vue)
|
||||
bus.$on('status-change', this.changeStatus)
|
||||
|
||||
},
|
||||
beforeDestroy(){
|
||||
bus.$off('status-change', this.changeStatus)
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.user
|
||||
},
|
||||
avatar() {
|
||||
return config.domain + "/avatars/" +this.$store.getters.user.avatar
|
||||
},
|
||||
getStatus() {
|
||||
return require(`./../../assets/status/${this.$store.getters.user.status || 0}.svg`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.show-status-list-enter-active, .show-status-list-leave-active {
|
||||
transition: .1s;
|
||||
}
|
||||
.show-status-list-enter, .show-status-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.my-mini-information {
|
||||
border-bottom: solid 1px rgb(218, 218, 218);
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-picture{
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.377);
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.information{
|
||||
color: white;
|
||||
margin-top: -7px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-icon{
|
||||
color: white;
|
||||
margin: auto;
|
||||
margin-right: 15px;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transition: 0.3s;
|
||||
|
||||
}
|
||||
.setting-icon:hover {
|
||||
background: rgba(0, 0, 0, 0.294);
|
||||
}
|
||||
.setting-icon .material-icons{
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding-top: 1px;
|
||||
padding-left: 5px;
|
||||
margin-left: 10px;
|
||||
transition: 0.3s;
|
||||
user-select: none;
|
||||
border-radius: 10px;
|
||||
|
||||
}
|
||||
|
||||
.status:hover {
|
||||
background: rgba(26, 25, 25, 0.349);
|
||||
}
|
||||
|
||||
.expand-status-icon{
|
||||
opacity: 0;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.status:hover .expand-status-icon{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status .current-status {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: 100%;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: rgb(199, 199, 199);
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
128
src/components/app/News.vue
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div class="news">
|
||||
<div class="change-log">
|
||||
<span class="news-title">Changes in this release</span>
|
||||
|
||||
<div class="change">
|
||||
<div class="date">{{changelog.date}}</div>
|
||||
<div class="changes-title">{{changelog.title}}</div>
|
||||
<div class="information">
|
||||
|
||||
<div v-if="changelog.new">
|
||||
<strong>What's new?</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wnew, index) in changelog.new" :key="index">{{wnew}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="changelog.fix">
|
||||
<strong>Issues fixed</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wfix, index) in changelog.fix" :key="index">{{wfix}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="changelog.next">
|
||||
<strong>Up next</strong><br>
|
||||
<ul>
|
||||
<li v-for="(wnext, index) in changelog.next" :key="index">{{wnext}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="changelog.msg">
|
||||
{{changelog.msg}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="todo-list">
|
||||
<span class="news-title">Planned Features</span>
|
||||
<p>Features that are coming soon:</p>
|
||||
<ul class="plan-list">
|
||||
<li>Online, Offline status (Done)</li>
|
||||
<li>Profile picture</li>
|
||||
<li>Typing indicator</li>
|
||||
<li>Sending files</li>
|
||||
<li>Custom emojis</li>
|
||||
<li>Guilds</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
import ChangeLog from '@/components/ChangeLog.vue'
|
||||
import changelog from '@/changelog.js'
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
ChangeLog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
changelog: changelog[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.news {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin:20px;
|
||||
color: white;
|
||||
overflow: auto;
|
||||
}
|
||||
.news-title {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: solid 1px white;
|
||||
}
|
||||
.todo-list{
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
background: rgba(0, 0, 0, 0.137);
|
||||
padding: 20px;
|
||||
}
|
||||
.change-log{
|
||||
background: rgba(0, 0, 0, 0.137);
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.plan-list{
|
||||
color: white;
|
||||
}
|
||||
.date{
|
||||
text-align: left;
|
||||
margin-right: 50px;
|
||||
color: rgba(255, 255, 255, 0.692);
|
||||
}
|
||||
.changes-title {
|
||||
font-size: 20px;
|
||||
color: rgba(255, 255, 255, 0.979);
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.news {
|
||||
flex-direction: column;
|
||||
}
|
||||
.todo-list{
|
||||
margin-left: 0;
|
||||
}
|
||||
.change-log {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
265
src/components/app/RightPanel.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<template>
|
||||
<div class="right-panel">
|
||||
<div class="heading">
|
||||
<div class="show-menu-button" cli>
|
||||
<i class="material-icons" @click="toggleLeftMenu">
|
||||
menu
|
||||
</i>
|
||||
</div>
|
||||
<div class="current-channel"><span v-if="!selectedChannelID">Welcome back!</span><span v-else>{{channelName}}</span></div>
|
||||
</div>
|
||||
<div class="loading" v-if="selectedChannelID && !messages[selectedChannelID]">
|
||||
<spinner />
|
||||
</div>
|
||||
<div v-else-if="selectedChannelID" class="message-logs">
|
||||
<message v-for="(msg, index) in messages[selectedChannelID]" :key="index" :username="msg.creator.username" :avatar="msg.creator.avatar" :message="msg.message" :status="msg.status" />
|
||||
</div>
|
||||
<news v-else />
|
||||
<div class="chat-input-area" v-if="selectedChannelID">
|
||||
<div class="message-area">
|
||||
<textarea class="chat-input" ref="input-box" placeholder="Message" @keydown="chatInput" @keyup="messageKeyUp" v-model="message"></textarea>
|
||||
<button :class="{'send-button': true, 'error-send-button': messageLength > 5000}" @click="sendMessage">Send</button>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div :class="{'message-count': true, 'error-info': messageLength > 5000 }">{{messageLength}}/5000</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messagesService from '@/services/messagesService'
|
||||
import {bus} from '../../main'
|
||||
import JQuery from 'jquery'
|
||||
let $ = JQuery
|
||||
import News from '../../components/app/News.vue'
|
||||
import Message from '../../components/app/MessageTemplate.vue'
|
||||
import Spinner from '@/components/Spinner.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
Spinner,
|
||||
News
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: "",
|
||||
messageLength: 0
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
toggleLeftMenu(){
|
||||
bus.$emit('toggleLeftMenu')
|
||||
},
|
||||
generateNum(n) {
|
||||
var add = 1, max = 12 - add; // 12 is the min safe number Math.random() can generate without it starting to pad the end with zeros.
|
||||
|
||||
if ( n > max ) {
|
||||
return this.generateNum(max) + this.generateNum(n - max);
|
||||
}
|
||||
|
||||
max = Math.pow(10, n+add);
|
||||
var min = max/10; // Math.pow(10, n) basically
|
||||
var number = Math.floor( Math.random() * (max - min + 1) ) + min;
|
||||
|
||||
return ("" + number).substring(add);
|
||||
},
|
||||
async sendMessage(){
|
||||
this.$refs["input-box"].focus()
|
||||
this.message = this.message.trim();
|
||||
|
||||
if(this.message == "")return;
|
||||
if (this.message.length > 5000) return;
|
||||
|
||||
const msg = this.message;
|
||||
const tempID = this.generateNum(25);
|
||||
|
||||
this.$store.dispatch('addMessage', {
|
||||
sender: true,
|
||||
channelID: this.selectedChannelID,
|
||||
message: {
|
||||
tempID,
|
||||
message: this.message,
|
||||
channelID: this.selectedChannelID
|
||||
}
|
||||
})
|
||||
|
||||
this.message = ""
|
||||
|
||||
const { ok, error, result } = await messagesService.post(this.selectedChannelID, {
|
||||
message: msg,
|
||||
socketID: this.$socket.id,
|
||||
tempID
|
||||
})
|
||||
if ( ok ) {
|
||||
const message = result.data.messageCreated
|
||||
message.status = 1;
|
||||
this.$store.dispatch('replaceMessage', {
|
||||
tempID: result.data.tempID,
|
||||
message
|
||||
});
|
||||
} else {
|
||||
// TODO: Error handling
|
||||
console.log(error)
|
||||
}
|
||||
},
|
||||
messageKeyUp(event){
|
||||
this.messageLength = this.message.length;
|
||||
},
|
||||
chatInput(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
},
|
||||
scrollDown(){
|
||||
//Scroll to bottom
|
||||
$(".message-logs").stop(true).animate({
|
||||
scrollTop: $(".message-logs")[0].scrollHeight
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
bus.$on('scrollDown', this.scrollDown);
|
||||
},
|
||||
beforeDestroy() {
|
||||
bus.$off('status-scrollDown', this.scrollDown)
|
||||
},
|
||||
computed: {
|
||||
messages() {
|
||||
return this.$store.getters.messages;
|
||||
},
|
||||
selectedChannelID() {
|
||||
return this.$store.getters.selectedChannelID
|
||||
},
|
||||
channelName() {
|
||||
return this.$store.getters.channelName;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.error-info {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.heading{
|
||||
border-bottom: solid 2px white;
|
||||
margin: 5px;
|
||||
height: 40px;
|
||||
padding-bottom: 2spx;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.show-menu-button{
|
||||
display: inline-block;
|
||||
margin: auto;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
margin-right: 5px;
|
||||
margin-top: 8px;
|
||||
user-select: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.heading .current-channel{
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
margin: auto;
|
||||
margin-left: 5px;
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.507);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.message-logs{
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.loading{
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.chat-input-area{
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.chat-input-area .info{
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-left: 25px;
|
||||
|
||||
}
|
||||
.message-area{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-input{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background: rgba(0, 0, 0, 0.158);
|
||||
color: white;
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
padding: 10px;
|
||||
margin: auto;
|
||||
margin-left: 20px;
|
||||
font-size: 15px;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding-left: 10px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.chat-input:hover{
|
||||
background: rgba(0, 0, 0, 0.288);
|
||||
}
|
||||
|
||||
.chat-input:focus{
|
||||
background: rgba(0, 0, 0, 0.466);
|
||||
}
|
||||
|
||||
.send-button{
|
||||
font-size: 20px;
|
||||
color:white;
|
||||
background: rgba(0, 0, 0, 0.274);
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: auto;
|
||||
margin-left: 2px;
|
||||
margin-right: 20px;
|
||||
height: 40px;
|
||||
transition: 0.3s;
|
||||
|
||||
}
|
||||
.send-button:hover{
|
||||
background: rgba(0, 0, 0, 0.514);
|
||||
}
|
||||
.error-send-button {
|
||||
background-color: rgba(255, 0, 0, 0.294);
|
||||
}
|
||||
|
||||
.error-send-button:hover {
|
||||
background-color: rgba(255, 0, 0, 0.294);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.show-menu-button{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
156
src/components/app/Settings.vue
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<div class="settings-darken-background">
|
||||
<div class="settings-box">
|
||||
<div class="tabs">
|
||||
|
||||
<div
|
||||
:class="{tab: true, selected: currentTab == 'my-profile'}"
|
||||
@click="tabClicked('my-profile','account_circle', 'My Profile')">
|
||||
<div class="material-icons">account_circle</div>
|
||||
<div>My Profile</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="{tab: true, selected: currentTab == 'ddddd'}"
|
||||
@click="tabClicked('ddddd','palette', 'Coming soon!')">
|
||||
<div class="material-icons">palette</div>
|
||||
<div>Message Themes</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="{tab: true, selected: currentTab == 'eee'}"
|
||||
@click="tabClicked('eee','error', 'Spoopi')">
|
||||
<div class="material-icons">error</div>
|
||||
<div>Another Tab</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="title">
|
||||
<div class="material-icons">{{icon}}</div>
|
||||
<div class="in-title">{{title}}</div>
|
||||
<div class="close-button" @click="close">
|
||||
<div class="material-icons">close</div>
|
||||
</div>
|
||||
</div>
|
||||
<component :is="currentTab"></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../../main'
|
||||
import MyProfile from './SettingsPanels/MyProfile.vue'
|
||||
export default {
|
||||
components: {
|
||||
MyProfile
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: "my-profile",
|
||||
icon: "account_circle",
|
||||
title: "My Profile"
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tabClicked(currentTab, icon, title) {
|
||||
this.currentTab = currentTab;
|
||||
this.icon = icon;
|
||||
this.title = title;
|
||||
},
|
||||
close() {
|
||||
bus.$emit('closeSettings');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.settings-darken-background{
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.541);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
color: white;
|
||||
}
|
||||
.settings-box{
|
||||
display: flex;
|
||||
margin: auto;
|
||||
box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.507);
|
||||
}
|
||||
.tabs{
|
||||
background: rgba(24, 24, 24, 0.938);
|
||||
height: 600px;
|
||||
width: 200px;
|
||||
}
|
||||
.panel {
|
||||
background: rgba(31, 31, 31, 0.924);
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
}
|
||||
.tab {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
background: rgba(26, 26, 26, 0.233);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab.selected {
|
||||
background: rgb(61, 61, 61) !important;
|
||||
}
|
||||
.tab div {
|
||||
margin: 5px;
|
||||
}
|
||||
.tab:hover {
|
||||
background: rgba(61, 61, 61, 0.616);
|
||||
}
|
||||
.title{
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
font-size: 25px;
|
||||
background: rgb(20, 20, 20);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title .material-icons{
|
||||
font-size: 40px;
|
||||
}
|
||||
.title div {
|
||||
margin: auto;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.in-title {
|
||||
flex:1;
|
||||
}
|
||||
.close-button{
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
padding: 5px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.close-button:hover{
|
||||
background: rgba(37, 37, 37, 0.692);
|
||||
}
|
||||
.close-button .material-icons {
|
||||
margin: auto;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 815px) {
|
||||
.settings-box{
|
||||
width:100%
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
209
src/components/app/SettingsPanels/MyProfile.vue
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<template>
|
||||
<div class="my-profile-panel">
|
||||
<div class="profile-picture" :style="`background-image: url(${avatar})`"></div>
|
||||
<div class="information">
|
||||
<div class="username"><strong>Username: </strong>{{user.username}}</div>
|
||||
<div class="tag"><strong>Tag: </strong>@{{user.tag}}</div>
|
||||
</div>
|
||||
<div class="options">
|
||||
<input type="file" accept="image/*" ref="avatarBrowser" @change="avatarBrowse" class="hidden">
|
||||
<div class="option" @click="$refs.avatarBrowser.click()">Edit Avatar</div>
|
||||
<div class="option" @click="changePassword">Change Password</div>
|
||||
<div class="option red" @click="logout">Logout</div>
|
||||
</div>
|
||||
<div class="alert-outer" v-if="alert.show">
|
||||
<div class="alert" >
|
||||
<div class="alert-title">Error</div>
|
||||
<div class="alert-content">
|
||||
{{alert.content}}
|
||||
</div>
|
||||
<div class="alert-buttons">
|
||||
<div class="alert-button" @click="alert.show = false">Okay</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UploadService from '@/services/UploadService.js'
|
||||
import config from '@/config.js'
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
|
||||
data(){
|
||||
return {
|
||||
alert: {
|
||||
content: 'Image size must be less than 10mb!',
|
||||
show: false
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onProgress(percent){
|
||||
//update vue
|
||||
console.log("Avatar upload progress: ", percent)
|
||||
},
|
||||
async avatarBrowse(event) {
|
||||
const file = event.target.files[0];
|
||||
const allowedFormats = ['.png', '.jpeg', '.gif', '.jpg' ];
|
||||
|
||||
if (!allowedFormats.includes(path.extname(file.name).toLowerCase())){
|
||||
this.alert.content = 'Unsupported image file.';
|
||||
return this.alert.show = true;
|
||||
} else if (file.size >= 10490000){
|
||||
// 10490000 = 10mb
|
||||
this.alert.content = 'Image size must be less than 10 megabytes.';
|
||||
return this.alert.show = true;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
const {ok, error, result} = await UploadService.uploadAvatar(formData, this.onProgress);
|
||||
if (!ok) {
|
||||
this.alert.content = 'Something went wrong. Try again later.';
|
||||
return this.alert.show = true;
|
||||
}
|
||||
|
||||
},
|
||||
logout() {
|
||||
this.$store.dispatch('logout')
|
||||
window.location.href = "/"
|
||||
},
|
||||
changePassword() {
|
||||
this.alert.content = 'Not implemented yet.';
|
||||
return this.alert.show = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.user
|
||||
},
|
||||
avatar() {
|
||||
return config.domain + "/avatars/" +this.$store.getters.user.avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.my-profile-panel{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.profile-picture {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgb(63, 63, 63);
|
||||
border-radius: 50%;
|
||||
margin-left: 20px;
|
||||
flex-shrink: 0;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.information {
|
||||
margin: auto;
|
||||
margin-left: 20px;
|
||||
margin-right: 0;
|
||||
font-size: 18px;
|
||||
flex: 1;
|
||||
}
|
||||
.username{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.options{
|
||||
margin: auto;
|
||||
margin-right: 30px;
|
||||
border-left: solid 1px rgb(204, 204, 204);
|
||||
padding-left: 10px;
|
||||
}
|
||||
.option {
|
||||
color: rgb(218, 218, 218);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.option:hover {
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.option.red {
|
||||
color: rgba(255, 0, 0, 0.678);
|
||||
}
|
||||
.option.red:hover {
|
||||
color: red;
|
||||
}
|
||||
.alert-title{
|
||||
background: rgb(34, 34, 34);
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
.alert-outer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.267)
|
||||
}
|
||||
.alert {
|
||||
margin: auto;
|
||||
background: rgb(49, 49, 49);
|
||||
width: 500px;
|
||||
box-shadow: 0px 0px 30px #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
.alert-content{
|
||||
margin: auto;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.alert-buttons {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
.alert-button {
|
||||
color: white;
|
||||
margin: auto;
|
||||
background: rgba(73, 53, 53, 0.712);
|
||||
padding: 10px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.alert-button:hover {
|
||||
background: rgb(83, 53, 53);
|
||||
}
|
||||
@media (max-width: 815px) {
|
||||
.my-profile-panel{
|
||||
flex-direction: column;
|
||||
}
|
||||
.profile-picture {
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.information {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
.options {
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
175
src/components/app/relationships/AddFriendPanel.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<div class="add-friend-panel">
|
||||
<div class="panel-title" @click="expanded = !expanded">
|
||||
<div>
|
||||
<i class="material-icons" v-if="!expanded">
|
||||
person_add
|
||||
</i>
|
||||
<span>{{expanded ? "Hide" : "Add friend"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="slide" appear>
|
||||
<div class="add-friend" v-if="expanded">
|
||||
<div class="title">Add friend</div>
|
||||
<div class="info">Type in your friends username and tag. eg: someone@jt4g</div>
|
||||
<div class="infoC">Creators tag: Fishie@azK0</div>
|
||||
<form action="#" @submit.prevent="addFriend">
|
||||
<input type="text" placeholder="username@tag" v-model="input">
|
||||
<loadingButton :loading="currentButtonMessage == 1" :message="buttonMessages[currentButtonMessage]" />
|
||||
</form>
|
||||
<div :class="{message: true, warning: errorMessage.isError}">
|
||||
{{errorMessage.message}}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RelationshipService from '@/services/RelationshipService.js'
|
||||
import loadingButton from './../../Button.vue'
|
||||
export default {
|
||||
components: {
|
||||
loadingButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
buttonMessages: [
|
||||
"Add Friend",
|
||||
"Adding..."
|
||||
],
|
||||
currentButtonMessage: 0,
|
||||
input: "",
|
||||
errorMessage:{
|
||||
message: "",
|
||||
isError: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async addFriend() {
|
||||
this.$set(this.errorMessage, 'message', "")
|
||||
|
||||
this.currentButtonMessage = 1;
|
||||
const split = this.input.trim().split("@");
|
||||
|
||||
// validation
|
||||
if ( split.length <2 || split.length >2 || split[1] === "" || split[1].length !== 4){
|
||||
this.$set(this.errorMessage, 'message', "Invalid username or tag.")
|
||||
this.$set(this.errorMessage, 'isError', true)
|
||||
this.currentButtonMessage = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const username = split[0];
|
||||
const tag = split[1];
|
||||
|
||||
const {ok, error, result} = await RelationshipService.post({username, tag})
|
||||
this.currentButtonMessage = 0;
|
||||
if ( ok ) {
|
||||
this.$set(this.errorMessage, 'message', result.data.message)
|
||||
this.$set(this.errorMessage, 'isError', false)
|
||||
} else {
|
||||
if (error.response === undefined) {
|
||||
this.$set(this.errorMessage, 'message', "Can't connect to server.")
|
||||
this.$set(this.errorMessage, 'isError', true)
|
||||
return
|
||||
}
|
||||
this.$set(this.errorMessage, 'message', error.response.data.errors[0].msg)
|
||||
this.$set(this.errorMessage, 'isError', true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: .3s;
|
||||
}
|
||||
.slide-enter, .slide-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
margin-bottom: -270px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.add-friend-panel{
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.123);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.add-friend{
|
||||
background: rgba(0, 0, 0, 0.13);
|
||||
flex: 1;
|
||||
height: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
.title{
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
.info{
|
||||
text-align: center;
|
||||
color: rgb(182, 182, 182);
|
||||
font-size: 15px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
.infoC{
|
||||
text-align: center;
|
||||
color: rgb(255, 79, 79);
|
||||
font-size: 15px;
|
||||
}
|
||||
form {
|
||||
margin: auto;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.message{
|
||||
margin: auto;
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
color: green;
|
||||
}
|
||||
.warning {
|
||||
color: red;
|
||||
}
|
||||
.panel-title{
|
||||
background: rgba(0, 0, 0, 0.274);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.panel-title:hover{
|
||||
background: rgba(0, 0, 0, 0.445);
|
||||
}
|
||||
|
||||
.panel-title .material-icons{
|
||||
vertical-align: top;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.panel-title span{
|
||||
margin-left: 5px;
|
||||
}
|
||||
.panel-title div{
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
64
src/components/app/relationships/Friends.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="friends" >
|
||||
<div class="tab" @click="expanded = !expanded">
|
||||
<Tab :expanded="expanded" tabname="Friends" />
|
||||
</div>
|
||||
<transition name="list" appear>
|
||||
<div class="list" v-if="expanded">
|
||||
<FriendsTemplate v-for="(friend, key) of friends" :key="key" :channelID="friend.channelID" :uniqueID="friends[key].recipient.uniqueID" :username="friend.recipient.username" :tag="friend.recipient.tag"/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from './Tab.vue'
|
||||
import FriendsTemplate from './FriendsTemplate.vue'
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
FriendsTemplate
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
friends() {
|
||||
const allFriend = this.$store.getters.user.friends;
|
||||
const result = Object.keys(allFriend).map(function(key) {
|
||||
return allFriend[key];
|
||||
});
|
||||
return result.filter(friend => friend.status == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: .3s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.friends{
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
margin: 5px;
|
||||
user-select: none;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab{
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab:hover{
|
||||
background-color: rgba(0, 0, 0, 0.123);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
131
src/components/app/relationships/FriendsTemplate.vue
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<div class="friend" @click="openChat">
|
||||
<div class="profile-picture" :style="`border-color: ${status.statusColor}; background-image: url(${userAvatar})`">
|
||||
<div class="status" :style="`background-image: url(${status.statusURL})`" ></div>
|
||||
</div>
|
||||
<div class="information">
|
||||
<div class="username">{{$props.username}}</div>
|
||||
<div class="status-name" :style="`color: ${status.statusColor}`">{{status.statusName}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import channelService from '@/services/channelService';
|
||||
import messagesService from '@/services/messagesService';
|
||||
import config from '@/config.js'
|
||||
import statuses from '@/statuses';
|
||||
|
||||
export default {
|
||||
props: ['username', 'tag', 'channelID', 'uniqueID'],
|
||||
methods: {
|
||||
async getMessages() {
|
||||
const {ok, error, result} = await messagesService.get(this.$props.channelID);
|
||||
if ( ok ) {
|
||||
this.$store.dispatch('messages', {channelID: result.data.channelID, messages: result.data.messages});
|
||||
} else {
|
||||
// TODO handle this
|
||||
console.log (error.response)
|
||||
}
|
||||
},
|
||||
async openChat() {
|
||||
this.$store.dispatch('selectedChannelID', this.$props.channelID);
|
||||
this.$store.dispatch('setName', this.$props.username);
|
||||
if (this.$store.getters.channels[this.$props.channelID]) return
|
||||
const {ok, error, result} = await channelService.post(this.$props.channelID);
|
||||
if ( ok ) {
|
||||
this.$store.dispatch('channel', result.data.channel);
|
||||
this.getMessages();
|
||||
} else {
|
||||
// TODO handle this
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.user.friends[this.$props.uniqueID].recipient;
|
||||
},
|
||||
userAvatar() {
|
||||
return config.domain + "/avatars/" + this.user.avatar
|
||||
},
|
||||
status() {
|
||||
const status = this.$store.getters.user.friends[this.$props.uniqueID].recipient.status || 0
|
||||
return {
|
||||
statusName: statuses[parseInt(status)].name,
|
||||
statusURL: statuses[parseInt(status)].url,
|
||||
statusColor: statuses[parseInt(status)].color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.friend {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.137);
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.friend:hover {
|
||||
background-color: rgba(0, 0, 0, 0.246);
|
||||
}
|
||||
|
||||
.profile-picture{
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-color: rgba(0, 0, 0, 0.425);
|
||||
border-radius: 50%;
|
||||
margin: auto;
|
||||
margin-left: 2px;
|
||||
margin-right: 5px;
|
||||
border: solid 3px;
|
||||
position: relative;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.information{
|
||||
margin: auto;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: black;
|
||||
border-radius: 50%;
|
||||
background-size: calc(100% + 2px);
|
||||
background-position: center;
|
||||
bottom: -10px;
|
||||
right: -5px;
|
||||
opacity: 0;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.friend:hover .status {
|
||||
opacity: 1;
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
.status-name{
|
||||
opacity: 0;
|
||||
font-size: 13px;
|
||||
transition: 0.3s;
|
||||
height: 0;
|
||||
}
|
||||
.friend:hover .status-name {
|
||||
opacity: 0.8;
|
||||
height: 19px;
|
||||
}
|
||||
</style>
|
||||
|
||||
64
src/components/app/relationships/PendingFriends.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="pending-friends" >
|
||||
<div class="tab" @click="expanded = !expanded">
|
||||
<Tab :expanded="expanded" tabname="Pending requests" />
|
||||
</div>
|
||||
<transition name="list" appear>
|
||||
<div class="list" v-if="expanded">
|
||||
<PendingTemplate v-for="(friend, key) of friends" :key="key" :uniqueID="friend.recipient.uniqueID" :status="friend.status" :username="friend.recipient.username" :tag="friend.recipient.tag"/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tab from './Tab.vue'
|
||||
import PendingTemplate from './PendingTemplate.vue'
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
PendingTemplate
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
friends() {
|
||||
const allFriend = this.$store.getters.user.friends;
|
||||
const result = Object.keys(allFriend).map(function(key) {
|
||||
return allFriend[key];
|
||||
});
|
||||
return result.filter(friend => friend.status < 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-enter-active, .list-leave-active {
|
||||
transition: .3s;
|
||||
}
|
||||
.list-enter, .list-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pending-friends{
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
margin: 5px;
|
||||
user-select: none;
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab{
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.tab:hover{
|
||||
background-color: rgba(0, 0, 0, 0.123);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
116
src/components/app/relationships/PendingTemplate.vue
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div class="pending-friend">
|
||||
<div class="profile-picture" :style="`background-image: url(${userAvatar})`"></div>
|
||||
<div class="information">
|
||||
<div class="username">{{$props.username}}</div>
|
||||
<div class="tag">@{{$props.tag}}</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div :class="{button: true, accept: true, hide: $props.status == 0}" @click="accept" >
|
||||
<i class="material-icons">
|
||||
check
|
||||
</i>
|
||||
</div>
|
||||
<div class="button decline" @click="deny">
|
||||
<i class="material-icons">
|
||||
not_interested
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RelationshipService from '@/services/RelationshipService.js'
|
||||
import config from '@/config.js'
|
||||
|
||||
export default {
|
||||
props: ['username', 'tag', 'status', 'uniqueID'],
|
||||
methods: {
|
||||
deny() {
|
||||
RelationshipService.delete(this.$props.uniqueID)
|
||||
},
|
||||
accept() {
|
||||
RelationshipService.put(this.$props.uniqueID)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.user.friends[this.$props.uniqueID].recipient;
|
||||
},
|
||||
userAvatar() {
|
||||
return config.domain + "/avatars/" + this.user.avatar
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.pending-friend {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.137);
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.pending-friend:hover {
|
||||
background-color: rgba(0, 0, 0, 0.246);
|
||||
}
|
||||
|
||||
.profile-picture{
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-color: rgba(0, 0, 0, 0.425);
|
||||
border-radius: 50%;
|
||||
margin: auto;
|
||||
margin-left: 2px;
|
||||
margin-right: 5px;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.information{
|
||||
margin: auto;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
.tag{
|
||||
color: rgb(173, 173, 173);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.buttons{
|
||||
display: flex;
|
||||
margin: auto;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: rgba(65, 65, 65, 0.438);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
.button:hover{
|
||||
background-color: rgba(0, 255, 0, 0.281);
|
||||
}
|
||||
.button .material-icons{
|
||||
margin: auto;
|
||||
color: rgba(255, 255, 255, 0.747);
|
||||
}
|
||||
.button.decline:hover{
|
||||
background-color: rgba(255, 0, 0, 0.281);
|
||||
}
|
||||
</style>
|
||||
|
||||
31
src/components/app/relationships/Tab.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="tab">
|
||||
<i :class="{'material-icons': true, closed: !$props.expanded}">
|
||||
expand_more
|
||||
</i>
|
||||
<div class="tab-name">
|
||||
{{$props.tabname}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['tabname', 'expanded']
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.tab{
|
||||
display: flex;
|
||||
color: white;
|
||||
}
|
||||
.material-icons{
|
||||
transition: 0.3s;
|
||||
}
|
||||
.closed{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
65
src/components/app/statusList.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="status-popout">
|
||||
<div class="status-list" @click="changeStatus(1)"><span class="status-icon"><img class="icon" :src="getStatusLogo(1)" /></span><span class="text">Online</span></div>
|
||||
<div class="status-list" @click="changeStatus(2)"><span class="status-icon"><img class="icon" :src="getStatusLogo(2)" /></span><span class="text">Away</span></div>
|
||||
<div class="status-list" @click="changeStatus(3)"><span class="status-icon"><img class="icon" :src="getStatusLogo(3)" /></span><span class="text">Busy</span></div>
|
||||
<div class="status-list" @click="changeStatus(4)"><span class="status-icon"><img class="icon" :src="getStatusLogo(4)" /></span><span class="text">Looking to play</span></div>
|
||||
<div class="status-list" @click="changeStatus(0)"><span class="status-icon"><img class="icon" :src="getStatusLogo(0)" /></span><span class="text">Offline</span></div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../../main';
|
||||
export default {
|
||||
methods: {
|
||||
getStatusLogo(status){
|
||||
return require(`./../../assets/status/${status}.svg`)
|
||||
},
|
||||
changeStatus(status) {
|
||||
bus.$emit('status-change', status)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.status-popout{
|
||||
position: absolute;
|
||||
background-color: rgba(44, 44, 44, 0.671);
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
width: 180px;
|
||||
}
|
||||
.status-list {
|
||||
padding: 5px;
|
||||
transition: 0.3s;
|
||||
border-radius: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.status-list:hover {
|
||||
background: rgba(0, 0, 0, 0.349);
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon{
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.text{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 9px;
|
||||
margin-left: 10px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
113
src/components/homePage/LoggedIn.vue
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="logged-in">
|
||||
<div class="title">Welcome!</div>
|
||||
<div class="card">
|
||||
<div class="avatar-outer">
|
||||
<div class="avatar" :style="`background-image: url(${avatar})`"></div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="username">{{user.username}}<span class="tag">@{{user.tag}}</span></div>
|
||||
<div class="buttons">
|
||||
<button class="button" @click="openChat">Enter</button>
|
||||
<button class="button logout" @click="logout">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '@/config.js'
|
||||
export default {
|
||||
methods: {
|
||||
logout() {
|
||||
this.$store.commit('logout')
|
||||
},
|
||||
openChat() {
|
||||
window.location.href = "/app"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.user;
|
||||
},
|
||||
avatar() {
|
||||
return config.domain + "/avatars/" +this.$store.getters.user.avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.logged-in {
|
||||
margin-top: 50px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card{
|
||||
background-color: rgba(0, 0, 0, 0.26);
|
||||
padding: 10px;
|
||||
margin: 20px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: table;
|
||||
margin: auto;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(0, 0, 0, 0.459);
|
||||
border-radius: 50%;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 20px;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.tag{
|
||||
font-size: 13px;
|
||||
color: rgb(194, 194, 194);
|
||||
}
|
||||
|
||||
.buttons{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.308);
|
||||
padding: 10px;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-right: 5px;
|
||||
margin-top: 10px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.button:hover {
|
||||
background: rgba(0, 0, 0, 0.582);
|
||||
}
|
||||
|
||||
.button.logout{
|
||||
background: rgba(149, 0, 0, 0.404);
|
||||
}
|
||||
.button.logout:hover {
|
||||
background: rgba(255, 0, 0, 0.582);
|
||||
}
|
||||
</style>
|
||||
103
src/components/homePage/LoginPanel.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div class="form">
|
||||
<form action="#" @submit.prevent="login">
|
||||
<div class="input">
|
||||
<div class="alert other-alert">{{otherError}}</div>
|
||||
<div class="input-title">Email: <div class="alert">{{email.alert}}</div></div>
|
||||
<input type="email" autocomplete="on" v-model="email.value">
|
||||
</div>
|
||||
<div class="input">
|
||||
<div class="input-title">Password: <div class="alert">{{password.alert}}</div></div>
|
||||
<input type="password" autocomplete="current-password" v-model="password.value">
|
||||
</div>
|
||||
<div class="cap">
|
||||
<recaptcha ref="recaptcha" @verify="captchaSubmit" />
|
||||
</div>
|
||||
<loadingButton :loading="currentMessage == 1" :message="buttonMessages[currentMessage]" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Recaptcha from '../Recaptcha.vue'
|
||||
import {bus} from '../../main';
|
||||
import loadingButton from "../../components/Button.vue"
|
||||
import AuthenticationService from '@/services/AuthenticationService';
|
||||
export default {
|
||||
components: {
|
||||
loadingButton,
|
||||
Recaptcha
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: {value: "", alert: ""},
|
||||
password: {value: "", alert: ""},
|
||||
otherError: "",
|
||||
buttonMessages: [
|
||||
"Login",
|
||||
"Logging in..."
|
||||
],
|
||||
currentMessage: 0,
|
||||
captcha: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetValues() {
|
||||
// Resets all of the alert values
|
||||
this.email.alert = "";
|
||||
this.password.alert = "";
|
||||
this.otherError = "";
|
||||
},
|
||||
async login() {
|
||||
this.currentMessage = 1
|
||||
this.resetValues();
|
||||
const email = this.email.value.trim();
|
||||
const password = this.password.value.trim();
|
||||
const captcha = this.captcha;
|
||||
|
||||
const {ok, error, result} = await AuthenticationService.login({email, password, token: captcha})
|
||||
this.currentMessage = 0
|
||||
|
||||
if (ok) {
|
||||
this.$store.dispatch('token', result.data.token)
|
||||
this.$store.dispatch('user', result.data.user)
|
||||
} else {
|
||||
this.captcha = null;
|
||||
this.$refs.recaptcha.resetRecaptcha();
|
||||
if (error.response === undefined) {
|
||||
this.otherError = "Can't connect to server."
|
||||
return;
|
||||
}
|
||||
const errors = error.response.data.errors;
|
||||
for (let index in errors) {
|
||||
const message = errors[index].msg;
|
||||
const param = errors[index].param;
|
||||
if(this[param] === undefined) {
|
||||
this.otherError = message;
|
||||
} else {
|
||||
this[param].alert = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
captchaSubmit(token) {
|
||||
this.captcha = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
display: table;
|
||||
margin: auto;
|
||||
}
|
||||
.cap {
|
||||
margin: 20px;
|
||||
opacity: 0.8;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.cap:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
121
src/components/homePage/RegisterPanel.vue
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="form">
|
||||
<form action="#" @submit.prevent="register">
|
||||
<div class="input">
|
||||
<div class="alert">{{otherError}}</div>
|
||||
<div class="input-title">Email: <div class="alert">{{email.alert}}</div></div>
|
||||
<input type="email" autocomplete="on" v-model="email.value">
|
||||
</div>
|
||||
<div class="input">
|
||||
<div class="input-title">Username: <div class="alert">{{username.alert}}</div></div>
|
||||
<input type="username" autocomplete="off" v-model="username.value">
|
||||
</div>
|
||||
<div class="input">
|
||||
<div class="input-title">Password: <div class="alert">{{password.alert}}</div></div>
|
||||
<input type="password" autocomplete="new-password" v-model="password.value">
|
||||
</div>
|
||||
<div class="input">
|
||||
<div class="input-title">Password confirm: <div class="alert">{{passwordConfirm.alert}}</div></div>
|
||||
<input type="password" autocomplete="new-password" v-model="passwordConfirm.value">
|
||||
</div>
|
||||
<div class="cap">
|
||||
<recaptcha ref="recaptcha" @verify="captchaSubmit" />
|
||||
</div>
|
||||
<loading-button :loading="currentMessage == 1" :message="buttonMessages[currentMessage]" />
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Recaptcha from '../Recaptcha.vue'
|
||||
import AuthenticationService from '@/services/AuthenticationService.js'
|
||||
import {bus} from '../../main';
|
||||
import LoadingButton from "../../components/Button.vue"
|
||||
export default {
|
||||
components: {
|
||||
LoadingButton,
|
||||
Recaptcha
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: {value: "", alert: ""},
|
||||
username: {value: "", alert: ""},
|
||||
password: {value: "", alert: ""},
|
||||
passwordConfirm: {value: "", alert: ""},
|
||||
otherError: "",
|
||||
buttonMessages: [
|
||||
"Register",
|
||||
"Creating... ( Thank you c: )"
|
||||
],
|
||||
currentMessage: 0,
|
||||
captcha: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetValues() {
|
||||
// Resets all of the alert values
|
||||
this.email.alert = "";
|
||||
this.username.alert = "";
|
||||
this.password.alert = "";
|
||||
this.passwordConfirm.alert = "";
|
||||
this.otherError = ""
|
||||
},
|
||||
async register() {
|
||||
this.currentMessage = 1
|
||||
this.resetValues();
|
||||
const email = this.email.value.trim();
|
||||
const username = this.username.value.trim();
|
||||
const password = this.password.value.trim();
|
||||
const passwordConfirm = this.passwordConfirm.value.trim();
|
||||
|
||||
// check if password + password confirm matches.
|
||||
if ( password != passwordConfirm ) {
|
||||
this.currentMessage = 0;
|
||||
return this.passwordConfirm.alert = "Passwords do not match!"
|
||||
}
|
||||
const {ok, error, result} = await AuthenticationService.register({email, username, password, token: this.captcha})
|
||||
this.currentMessage = 0
|
||||
if ( ok ) {
|
||||
this.$store.dispatch('token', result.data.token)
|
||||
this.$store.dispatch('user', result.data.user)
|
||||
} else {
|
||||
this.captcha = null;
|
||||
this.$refs.recaptcha.resetRecaptcha();
|
||||
if (error.response === undefined) {
|
||||
this.otherError = "Can't connect to server."
|
||||
return;
|
||||
}
|
||||
const errors = error.response.data.errors;
|
||||
for (let index in errors) {
|
||||
const message = errors[index].msg;
|
||||
const param = errors[index].param;
|
||||
if(this[param] === undefined) {
|
||||
this.otherError = message;
|
||||
} else {
|
||||
this[param].alert = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
captchaSubmit(token) {
|
||||
this.captcha = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
display: table;
|
||||
margin: auto;
|
||||
}
|
||||
.cap {
|
||||
margin: 20px;
|
||||
opacity: 0.8;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.cap:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
186
src/components/homePage/RightPanel.vue
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<div class="right-panel-home">
|
||||
<div class="right-panel-inner">
|
||||
<div class="logo"></div>
|
||||
<div class="title">Nertivia</div>
|
||||
<spinner :msg="spinnerMessage" v-if="previouslyLoggedIn && user == null && tokenExists" />
|
||||
|
||||
<transition name="component-fade" appear mode="out-in" v-else>
|
||||
|
||||
<logged-in v-if="tokenExists && user != null" />
|
||||
|
||||
<div class="new-member" v-if="!tokenExists">
|
||||
<div class="details">
|
||||
Nertivia chat is the best chat client to be made with 99% uptime, you won't miss a thing! Join now if you’re new!
|
||||
</div>
|
||||
<div class="switch-buttons">
|
||||
<div :class="{button: true, selected: loginSelected}" @click="loginSelected = true">Already a pro</div>
|
||||
<div :class="{button: true, selected: !loginSelected}" @click="loginSelected = false">I'm new!</div>
|
||||
</div>
|
||||
<transition name="switch-selected" mode="out-in">
|
||||
<login-panel v-if="loginSelected" />
|
||||
<register-panel v-if="!loginSelected" />
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../../main';
|
||||
import AuthenticationService from '@/services/AuthenticationService.js'
|
||||
import RegisterPanel from "../../components/homePage/RegisterPanel.vue"
|
||||
import LoginPanel from "../../components/homePage/LoginPanel.vue"
|
||||
import LoggedIn from "../../components/homePage/LoggedIn.vue"
|
||||
import Spinner from "../../components/Spinner.vue"
|
||||
export default {
|
||||
components: {
|
||||
RegisterPanel,
|
||||
LoginPanel,
|
||||
LoggedIn,
|
||||
Spinner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loginSelected: true,
|
||||
previouslyLoggedIn: false,
|
||||
spinnerMessage: "Logging in...",
|
||||
connectionRetryCount: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getUser() {
|
||||
// Get details if previously logged in.
|
||||
if (this.previouslyLoggedIn) {
|
||||
const { ok, error, result } = await AuthenticationService.user();
|
||||
if ( ok ) {
|
||||
this.$store.commit( 'user', result.data.user );
|
||||
} else {
|
||||
if ( error.response === undefined ) {
|
||||
this.connectionRetryCount++;
|
||||
this.spinnerMessage = `Connection failed. Trying again (${this.connectionRetryCount})`
|
||||
setTimeout(() => {
|
||||
this.getUser();
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
this.$store.commit( 'logout' );
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.previouslyLoggedIn = this.tokenExists;
|
||||
this.getUser()
|
||||
|
||||
},
|
||||
computed: {
|
||||
tokenExists() {
|
||||
return this.$store.getters.tokenExists;
|
||||
},
|
||||
user() {
|
||||
return this.$store.getters.user;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.component-fade-enter-active, .component-fade-leave-active {
|
||||
transition: .3s ease;
|
||||
}
|
||||
.component-fade-enter, .component-fade-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.switch-selected-enter-active, .switch-selected-leave-active {
|
||||
transition: .3s ease;
|
||||
}
|
||||
.switch-selected-enter, .switch-selected-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.right-panel-home {
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.493);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
user-select: none;
|
||||
}
|
||||
.right-panel-inner{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.new-member{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
background: url(./../../assets/logo.png);
|
||||
background-size: 129%;
|
||||
background-position: center;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
|
||||
margin: auto;
|
||||
margin-bottom: 0;
|
||||
margin-top: 40px;
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
.title{
|
||||
color: white;
|
||||
font-size: 35px;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.right-panel .title {
|
||||
margin: auto;
|
||||
margin-top: 10px;
|
||||
width: 230px;
|
||||
font-size: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.details{
|
||||
color: rgb(204, 204, 204);
|
||||
margin: 49px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.switch-buttons{
|
||||
display: table;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.button{
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
display: inline-block;
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
user-select: none;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.button:hover{
|
||||
border-bottom: solid 2px rgba(255, 255, 255, 0.493);
|
||||
}
|
||||
|
||||
.button.selected{
|
||||
border-bottom: solid 2px white;
|
||||
}
|
||||
|
||||
</style>
|
||||
32
src/config.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
const config = {
|
||||
devMode:true,
|
||||
recaptcha: "",
|
||||
IP: [
|
||||
{
|
||||
domain: "http://api.localhost",
|
||||
socketIP: "localhost"
|
||||
},
|
||||
{
|
||||
domain: "https://api.supertiger.tk",
|
||||
socketIP: "https://nertivia.supertiger.tk"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (window.webpackHotUpdate) {
|
||||
config.devMode = true;
|
||||
} else {
|
||||
config.devMode = false
|
||||
}
|
||||
|
||||
if ( config.devMode ) {
|
||||
config.recaptcha = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI";
|
||||
config['domain'] = config.IP[0].domain;
|
||||
config['socketIP'] = config.IP[0].socketIP;
|
||||
} else {
|
||||
config.recaptcha = "6Ld0EIwUAAAAALJNTa-1s63l-w_jHyCY6dFAVwKe";
|
||||
config['domain'] = config.IP[1].domain;
|
||||
config['socketIP'] = config.IP[1].socketIP;
|
||||
}
|
||||
|
||||
export default config;
|
||||
26
src/main.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import Vue from 'vue'
|
||||
import {router} from './router'
|
||||
import Main from '../src/Main.vue'
|
||||
import {store} from './store/index';
|
||||
import Axios from 'axios';
|
||||
import './clickOutside';
|
||||
import vueHeadful from 'vue-headful';
|
||||
Vue.component('vue-headful', vueHeadful);
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
const token = localStorage.getItem('hauthid');
|
||||
Vue.prototype.$http = Axios;
|
||||
|
||||
if (token) {
|
||||
Vue.prototype.$http.defaults.headers.common['authorization'] = token;
|
||||
}
|
||||
|
||||
export const bus = new Vue();
|
||||
|
||||
|
||||
new Vue({
|
||||
store,
|
||||
router,
|
||||
render: h => h(Main)
|
||||
}).$mount('#app')
|
||||
47
src/router.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
Vue.use(VueRouter)
|
||||
import {store} from './store/index';
|
||||
import MainApp from '../src/views/App.vue'
|
||||
import HomePage from '../src/views/HomePage.vue'
|
||||
import VueSocketio from 'vue-socket.io-extended';
|
||||
import io from 'socket.io-client';
|
||||
import config from './config'
|
||||
import VueMq from 'vue-mq'
|
||||
|
||||
export const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'HomePage',
|
||||
component: HomePage
|
||||
},
|
||||
{
|
||||
path: '/app',
|
||||
name: 'app',
|
||||
component: MainApp,
|
||||
beforeEnter (to, from, next) {
|
||||
if (!localStorage.getItem('hauthid')){
|
||||
return router.push({ path: '/' })
|
||||
}
|
||||
Vue.use(VueSocketio, io(config.socketIP, {
|
||||
transportOptions: {
|
||||
polling: {
|
||||
extraHeaders: {
|
||||
authorization: localStorage.getItem('hauthid')
|
||||
}
|
||||
}
|
||||
}
|
||||
}), {store});
|
||||
Vue.use(VueMq, {
|
||||
breakpoints: {
|
||||
mobile: 600,
|
||||
desktop: Infinity,
|
||||
}
|
||||
})
|
||||
next()
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
14
src/services/Api.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import axios from 'axios';
|
||||
import config from '@/config';
|
||||
|
||||
export const instance = () => {
|
||||
return axios.create({
|
||||
baseURL: config.domain
|
||||
})
|
||||
}
|
||||
|
||||
export const wrapper = (promise) => {
|
||||
return promise
|
||||
.then(result => ({ok: true, result}))
|
||||
.catch(error => Promise.resolve({ok: false, error}))
|
||||
}
|
||||
13
src/services/AuthenticationService.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
register ( credentials ) {
|
||||
return wrapper(instance().post('user/register', credentials));
|
||||
},
|
||||
login( credentials ) {
|
||||
return wrapper(instance().post('user/login', credentials));
|
||||
},
|
||||
user () {
|
||||
return wrapper(instance().get('user/details'))
|
||||
}
|
||||
}
|
||||
18
src/services/RelationshipService.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
post ( friend ) {
|
||||
return wrapper(instance().post('/user/relationship', friend));
|
||||
},
|
||||
put( uniqueID ) {
|
||||
return wrapper(instance().put('/user/relationship', {uniqueID}));
|
||||
},
|
||||
delete( uniqueID ) {
|
||||
return wrapper(instance().delete(
|
||||
'/user/relationship',
|
||||
{
|
||||
data: {uniqueID}
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
19
src/services/UploadService.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
uploadAvatar(data, onProgress){
|
||||
const url = `/settings/avatar`;
|
||||
let config = {
|
||||
onUploadProgress(progressEvent) {
|
||||
var percentCompleted = Math.round((progressEvent.loaded * 100) /
|
||||
progressEvent.total);
|
||||
|
||||
// execute the callback
|
||||
if (onProgress) onProgress(percentCompleted)
|
||||
|
||||
return percentCompleted;
|
||||
},
|
||||
};
|
||||
return wrapper(instance().post(url, data, config));
|
||||
}
|
||||
}
|
||||
7
src/services/channelService.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
post ( channelID ) {
|
||||
return wrapper(instance().post(`channels/${channelID}`));
|
||||
}
|
||||
}
|
||||
12
src/services/messagesService.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
// TODO: add ?continue=id
|
||||
get ( channelID ) {
|
||||
return wrapper(instance().get(`messages/${channelID}`));
|
||||
},
|
||||
// TODO tempID
|
||||
post (channelID, data) {
|
||||
return wrapper(instance().post(`messages/${channelID}`, data))
|
||||
}
|
||||
}
|
||||
7
src/services/settingsService.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
setStatus ( status ) {
|
||||
return wrapper(instance().post('/settings/status', { status }));
|
||||
},
|
||||
}
|
||||
5
src/services/userService.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import {instance, wrapper} from './Api';
|
||||
|
||||
export default {
|
||||
|
||||
}
|
||||
35
src/statuses.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const config = [
|
||||
//Offline
|
||||
{
|
||||
name: "Offline",
|
||||
url: require("@/assets/status/0.svg"),
|
||||
color: "#919191"
|
||||
},
|
||||
//Online
|
||||
{
|
||||
name: "Online",
|
||||
url: require("@/assets/status/1.svg"),
|
||||
color: "#27eb48"
|
||||
},
|
||||
//Away
|
||||
{
|
||||
name: "Away",
|
||||
url: require("@/assets/status/2.svg"),
|
||||
color: "#ffdd1e"
|
||||
},
|
||||
//Busy
|
||||
{
|
||||
name: "Busy",
|
||||
url: require("@/assets/status/3.svg"),
|
||||
color: "#ea0b1e"
|
||||
},
|
||||
//Looking to play
|
||||
{
|
||||
name: "Looking to play",
|
||||
url: require("@/assets/status/4.svg"),
|
||||
color: "#9a3dd3"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export default config;
|
||||
31
src/store/index.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import user from './modules/userModule';
|
||||
import socketModule from './modules/socketIOModule';
|
||||
import channelModule from './modules/channelModule';
|
||||
import messageModule from './modules/messageModule';
|
||||
import {router} from './../router'
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const store = new Vuex.Store({
|
||||
modules: { user, socketModule, channelModule, messageModule },
|
||||
state: {
|
||||
|
||||
},
|
||||
getters: {
|
||||
|
||||
},
|
||||
mutations: {
|
||||
sendMessage(state, message) {
|
||||
if (state.messageLogs[state.channelID] === undefined) {
|
||||
state.messageLogs[state.channelID] = {};
|
||||
}
|
||||
state.messageLogs[state.channelID][Date.now().toString()] = {channelID: state.channelID, message: message, messageID: Date.now(), status: 0};
|
||||
}
|
||||
|
||||
},
|
||||
actions: {
|
||||
|
||||
}
|
||||
})
|
||||
52
src/store/modules/channelModule.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import {bus} from '../../main'
|
||||
import {router} from './../../router'
|
||||
|
||||
const state = {
|
||||
selectedChannelID: null,
|
||||
channelName: null,
|
||||
channels: {}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
selectedChannelID(state) {
|
||||
return state.selectedChannelID;
|
||||
},
|
||||
channels(state) {
|
||||
return state.channels;
|
||||
},
|
||||
channelName(state) {
|
||||
return state.channelName;
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
channel(context, channel) {
|
||||
context.commit('channel', channel)
|
||||
},
|
||||
selectedChannelID(context, channelID) {
|
||||
context.commit('selectedChannelID', channelID)
|
||||
},
|
||||
setName(context, name) {
|
||||
context.commit('setName', name)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
channel(state, channel) {
|
||||
state.channels[channel.channelID] = channel;
|
||||
},
|
||||
selectedChannelID(state, channelID) {
|
||||
state.selectedChannelID = channelID;
|
||||
},
|
||||
setName(state, name) {
|
||||
state.channelName = name;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespace: true,
|
||||
state,
|
||||
actions,
|
||||
mutations,
|
||||
getters
|
||||
}
|
||||
71
src/store/modules/messageModule.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import {bus} from '../../main'
|
||||
import {router} from '../../router'
|
||||
import Vue from 'vue';
|
||||
|
||||
const state = {
|
||||
messages: {}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
messages(state) {
|
||||
return state.messages;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const actions = {
|
||||
messages(context, data) {
|
||||
context.commit('messages', data)
|
||||
},
|
||||
addMessage(context, data) {
|
||||
if (data.sender) {
|
||||
data.message.creator = context.getters.user
|
||||
data.message.status = 0;
|
||||
}
|
||||
context.commit('addMessage', data);
|
||||
},
|
||||
replaceMessage(context, data) {
|
||||
context.commit('replaceMessage', data)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
messages(state, data) {
|
||||
Vue.set(state.messages, data.channelID, data.messages.reverse())
|
||||
setTimeout(() => {
|
||||
bus.$emit('scrollDown');
|
||||
}, 300);
|
||||
},
|
||||
addMessage(state, data) {
|
||||
bus.$emit('scrollDown');
|
||||
Vue.set(
|
||||
state.messages[data.channelID],
|
||||
state.messages[data.channelID].length,
|
||||
data.message
|
||||
);
|
||||
},
|
||||
|
||||
replaceMessage (state, data) {
|
||||
const {tempID, message} = data;
|
||||
|
||||
state.messages[message.channelID].find((o, i) => {
|
||||
if(o.tempID === tempID) {
|
||||
Vue.set(
|
||||
state.messages[message.channelID],
|
||||
i,
|
||||
message
|
||||
)
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespace: true,
|
||||
state,
|
||||
actions,
|
||||
mutations,
|
||||
getters
|
||||
}
|
||||
70
src/store/modules/socketIOModule.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {bus} from '../../main'
|
||||
import {router} from './../../router'
|
||||
|
||||
const state = {
|
||||
|
||||
}
|
||||
|
||||
const actions = {
|
||||
socket_error(context, error) {
|
||||
// if the token is invalid.
|
||||
if (error === "Authentication error") {
|
||||
context.commit('logout')
|
||||
router.push({ path: '/' })
|
||||
}
|
||||
},
|
||||
socket_success(context, data) {
|
||||
const friendsArray = data.user.friends;
|
||||
const friendObject = {};
|
||||
if(friendsArray !== undefined && friendsArray.length >=1) {
|
||||
for (let index = 0; index < friendsArray.length; index++) {
|
||||
const element = friendsArray[index];
|
||||
friendObject[element.recipient.uniqueID] = element;
|
||||
for (let currentFriendStatus of data.currentFriendStatus){
|
||||
if(currentFriendStatus[0] == element.recipient.uniqueID){
|
||||
friendObject[element.recipient.uniqueID].recipient.status = currentFriendStatus[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
data.user.friends = friendObject;
|
||||
}
|
||||
context.commit('user', data.user)
|
||||
|
||||
},
|
||||
socket_relationshipAdd(context, friend) {
|
||||
context.commit('addFriend', friend)
|
||||
},
|
||||
socket_relationshipAccept(context, uniqueID) {
|
||||
context.commit('acceptFriend', uniqueID)
|
||||
},
|
||||
socket_relationshipRemove(context, uniqueID) {
|
||||
context.commit('removeFriend', uniqueID)
|
||||
},
|
||||
socket_receiveMessage(context, data) {
|
||||
context.dispatch('addMessage', {
|
||||
message: data.message,
|
||||
channelID: data.message.channelID,
|
||||
tempID: data.tempID
|
||||
})
|
||||
},
|
||||
socket_userStatusChange(context, data) {
|
||||
context.commit('userStatusChange', data)
|
||||
},
|
||||
socket_multiDeviceStatus(context, data) {
|
||||
context.commit('changeStatus', data.status)
|
||||
},
|
||||
socket_disconnect(context) {
|
||||
context.commit('user', null)
|
||||
},
|
||||
socket_multiDeviceUserAvatarChange(context, data) {
|
||||
context.commit('changeAvatar', data.avatarID);
|
||||
},
|
||||
socket_userAvatarChange(context, data) {
|
||||
context.commit('userAvatarChange', data)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespace: true,
|
||||
actions
|
||||
}
|
||||
101
src/store/modules/userModule.js
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import axios from 'axios'
|
||||
import Vue from 'vue'
|
||||
import {bus} from '../../main'
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
const state = {
|
||||
token: localStorage.getItem('hauthid') || null,
|
||||
channelID: "",
|
||||
user: null,
|
||||
}
|
||||
|
||||
const getters = {
|
||||
tokenExists(state) {
|
||||
return state.token !== null;
|
||||
},
|
||||
user(state){
|
||||
return state.user;
|
||||
},
|
||||
loggedIn(state){
|
||||
return state.user !== null
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
token(context, token) {
|
||||
context.commit('token', token);
|
||||
},
|
||||
user(context, user) {
|
||||
context.commit('user', user)
|
||||
},
|
||||
logout(context) {
|
||||
context.commit('logout');
|
||||
},
|
||||
changeStatus(context, status) {
|
||||
context.commit('changeStatus', status)
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
changeAvatar(state, avatar) {
|
||||
//changes my avatar
|
||||
Vue.set(state.user, "avatar", avatar)
|
||||
},
|
||||
changeStatus(state, status) {
|
||||
//changes my status
|
||||
Vue.set(state.user, "status", status)
|
||||
},
|
||||
userStatusChange(state, data) {
|
||||
// changes friends status
|
||||
const friends = state.user.friends;
|
||||
friends[data.uniqueID].recipient.status = data.status;
|
||||
state.user.friends = Object.assign({}, friends)
|
||||
},
|
||||
userAvatarChange(state, data) {
|
||||
// changes friends status
|
||||
const friends = state.user.friends;
|
||||
friends[data.uniqueID].recipient.avatar = data.avatarID;
|
||||
state.user.friends = Object.assign({}, friends)
|
||||
},
|
||||
token(state, token) {
|
||||
axios.defaults.headers.common['authorization'] = token;
|
||||
localStorage.setItem('hauthid', token);
|
||||
state.token = token
|
||||
},
|
||||
logout(state) {
|
||||
axios.defaults.headers.common['authorization'] = "";
|
||||
localStorage.removeItem('hauthid')
|
||||
state.user = null,
|
||||
state.token = null;
|
||||
},
|
||||
user(state, user) {
|
||||
Vue.set(state, 'user', user)
|
||||
},
|
||||
addFriend(state, friend) {
|
||||
|
||||
const friends = state.user.friends;
|
||||
friends[friend.recipient.uniqueID] = friend;
|
||||
|
||||
state.user.friends = Object.assign({}, friends)
|
||||
},
|
||||
removeFriend(state, uniqueID) {
|
||||
const friends = state.user.friends;
|
||||
delete friends[uniqueID];
|
||||
|
||||
state.user.friends = Object.assign({}, friends)
|
||||
},
|
||||
acceptFriend(state, uniqueID) {
|
||||
const friends = state.user.friends;
|
||||
friends[uniqueID].status = 2;
|
||||
|
||||
state.user.friends = Object.assign({}, friends)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespace: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
174
src/views/App.vue
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<vue-headful title="Nertivia"/>
|
||||
<div class="background-image"></div>
|
||||
<transition name="fade-between-two" appear >
|
||||
<ConnectingScreen v-if="!loggedIn" />
|
||||
<div class="box" v-if="loggedIn">
|
||||
<div class="panel-layout">
|
||||
<transition name="slidein">
|
||||
<LeftPanel class="left-panel" v-click-outside="hideLeftPanel" v-show="$mq === 'mobile' && showLeftPanel || $mq === 'desktop'"> </LeftPanel>
|
||||
</transition>
|
||||
<RightPanel> </RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<settings v-if="showSettings && loggedIn" />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../main'
|
||||
import Settings from '@/components/app/Settings.vue'
|
||||
import LeftPanel from './../components/app/LeftPanel.vue'
|
||||
import RightPanel from './../components/app/RightPanel.vue'
|
||||
import ConnectingScreen from './../components/app/ConnectingScreen.vue'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
LeftPanel,
|
||||
RightPanel,
|
||||
ConnectingScreen,
|
||||
Settings
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showLeftPanel: false,
|
||||
showSettings: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideLeftPanel(test) {
|
||||
if (this.showLeftPanel){
|
||||
if(test.target.closest('.show-menu-button') == null){
|
||||
this.showLeftPanel = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
bus.$on('toggleLeftMenu', () => {
|
||||
this.showLeftPanel = !this.showLeftPanel;
|
||||
})
|
||||
bus.$on('openSettings', () => {
|
||||
this.showSettings = true;
|
||||
})
|
||||
bus.$on('closeSettings', () => {
|
||||
this.showSettings = false;
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
loggedIn() {
|
||||
return this.$store.getters.loggedIn
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.slidein-enter-active, .slidein-leave-active {
|
||||
transition: .5s;
|
||||
}
|
||||
.slidein-enter, .slidein-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
margin-left: -300px;
|
||||
}
|
||||
|
||||
.fade-between-two-enter-active, .fade-between-two-leave-active{
|
||||
transition: 0.3s;
|
||||
}
|
||||
.fade-between-two-enter, .fade-between-two-leave-to {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.left-panel{
|
||||
position: absolute;
|
||||
top: 47px;
|
||||
height: calc(100% - 47px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
html{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #383838;
|
||||
height: 100%;
|
||||
}
|
||||
.box {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.background-image {
|
||||
background: url(./../assets/background.jpg);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
background-size: cover;
|
||||
|
||||
}
|
||||
|
||||
.panel-layout {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* ------- SCROLL BAR -------*/
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #8080806b;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #f5f5f559;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #f5f5f59e;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
432
src/views/HomePage.vue
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<vue-headful title="Nertivia" />
|
||||
<div class="background-image"></div>
|
||||
<div class="layout">
|
||||
<div class="small-view-nav-bar">
|
||||
<div class="small-logo"></div>
|
||||
<div class="small-title">Nertivia</div>
|
||||
<div class="show-menu-button" @click="showMobileMenu = !showMobileMenu">
|
||||
<i class="material-icons">
|
||||
menu
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panels">
|
||||
<div class="left-panel">
|
||||
|
||||
<div class="title">The best chat client that won’t sell your data.</div>
|
||||
<img src="../assets/graphics/HomeGraphics.png" class="graphic-app" />
|
||||
<div class="change-log-mini" @click="showChangeLog = true">
|
||||
<div class="change-title">Change log <span style="font-size: 15px; color: rgba(211, 211, 211, 0.774);">Click for details</span></div>
|
||||
<div class="change-list">
|
||||
<div class="change" v-for="change in changelogFiltered" :key="change.title">
|
||||
<div class="notable-changes">{{change.shortTitle}}</div>
|
||||
<div class="change-date">{{change.date}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightPanel :class="{'show-menu-content': showMobileMenu }" />
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<ChangeLog v-if="showChangeLog"/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {bus} from '../main';
|
||||
import RightPanel from "./../components/homePage/RightPanel.vue"
|
||||
import ChangeLog from "./../components/ChangeLog.vue"
|
||||
import changelog from '@/changelog.js'
|
||||
export default {
|
||||
components: {
|
||||
RightPanel,
|
||||
ChangeLog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loginSelected: true,
|
||||
showMobileMenu: false,
|
||||
showChangeLog: false,
|
||||
changelog
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
bus.$on('closeChangeLog', () => {
|
||||
this.showChangeLog = false;
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
changelogFiltered() {
|
||||
return this.changelog.slice(0, 3)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .2s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: #383838;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spinner{
|
||||
margin: auto;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.background-image {
|
||||
background: url(./../assets/background.jpg);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
background-size: cover;
|
||||
|
||||
}
|
||||
|
||||
.layout{
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width:100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panels{
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.left-panel {
|
||||
flex: 1;
|
||||
background: rgba(0, 0, 0, 0.253);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loader{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
.title-panel{
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.graphics-panel{
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.graphic-app{
|
||||
display: table;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
width: 900px;
|
||||
height: auto;
|
||||
user-select: none;
|
||||
}
|
||||
.title{
|
||||
color: white;
|
||||
font-size: 35px;
|
||||
text-align: center;
|
||||
margin-top: 120px;
|
||||
}
|
||||
|
||||
.change-log-mini{
|
||||
background: rgba(0, 0, 0, 0.322);
|
||||
height: 150px;
|
||||
width: 640px;
|
||||
margin: auto;
|
||||
margin-top: 20px;
|
||||
color: white;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.change-title {
|
||||
font-size: 18px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 10px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.change-list{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.change {
|
||||
background: rgba(0, 0, 0, 0.335);
|
||||
width: 200px;
|
||||
height: 90px;
|
||||
margin-left: 10px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
position: relative
|
||||
}
|
||||
.change:hover {
|
||||
background: rgba(0, 0, 0, 0.466);
|
||||
}
|
||||
.notable-changes{
|
||||
margin: 10px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.change-date{
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
color: rgba(255, 255, 255, 0.753);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.small-view-nav-bar{
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: rgba(0, 0, 0, 0.411);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
}
|
||||
|
||||
.small-logo {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
background: url(./../assets/logo.png);
|
||||
background-size: 125%;
|
||||
background-position: center;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 96px -4px rgba(69,212,255,1);
|
||||
margin: auto;
|
||||
margin-left: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.small-title{
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.show-menu-button{
|
||||
color: rgba(255, 255, 255, 0.698);
|
||||
margin-right: 20px;
|
||||
margin-top: 7px;
|
||||
user-select: none;
|
||||
transition: 0.3s
|
||||
}
|
||||
|
||||
.show-menu-button:hover {
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.show-menu-content {
|
||||
display: flex !important;
|
||||
width: 400px !important;
|
||||
opacity: 1 !important;
|
||||
transform: scale(1) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1051px) {
|
||||
.change:nth-child(3){
|
||||
display: none;
|
||||
}
|
||||
.change-log-mini{
|
||||
width: 430px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 906px) {
|
||||
|
||||
.change:nth-child(3){
|
||||
display: block;
|
||||
}
|
||||
.change-log-mini{
|
||||
width: 640px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 649px) {
|
||||
.change-list{
|
||||
flex-direction: column;
|
||||
}
|
||||
.change-log-mini{
|
||||
height: initial;
|
||||
width: calc(100% - 20px);
|
||||
padding-bottom: 10px;
|
||||
margin: auto;
|
||||
}
|
||||
.change{
|
||||
margin-bottom: 5px;
|
||||
margin-left: 5px;
|
||||
margin-right: 0;
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
@media (max-width: 1380px) {
|
||||
.graphic-app{
|
||||
width: calc(100% - 80px);
|
||||
}
|
||||
.title{
|
||||
font-size: 30px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
|
||||
}
|
||||
}
|
||||
@media (max-width: 906px) {
|
||||
.right-panel-home {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 50px;
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
height:calc(100% - 50px);
|
||||
background-color: rgba(34, 34, 34, 0.877);
|
||||
width: 0;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s ease;
|
||||
transform: scale(0.97);
|
||||
opacity: 0;
|
||||
|
||||
}
|
||||
.right-panel-inner{
|
||||
width: 400px;
|
||||
}
|
||||
.small-view-nav-bar{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 401px) {
|
||||
.show-menu-content {
|
||||
width: 100% !important;
|
||||
}
|
||||
.right-panel-inner{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<!-- Used for forms !-->
|
||||
<style>
|
||||
@media (max-width: 1380px) {
|
||||
.graphic-app{
|
||||
width: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 906px) {
|
||||
|
||||
|
||||
|
||||
.right-panel-home {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: 50px;
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
margin-top: 0;
|
||||
height:calc(100% - 50px);
|
||||
background-color: rgba(34, 34, 34, 0.877);
|
||||
width: 0;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s ease;
|
||||
transform: scale(0.97);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.right-panel-inner{
|
||||
width: 400px;
|
||||
}
|
||||
.small-view-nav-bar{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
@media (max-width: 401px) {
|
||||
.show-menu-content {
|
||||
width: 100% !important;
|
||||
}
|
||||
.right-panel-inner{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
color: white;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
input{
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.301);
|
||||
outline: none;
|
||||
border: none;
|
||||
color: white;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
width: 200px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
input:hover{
|
||||
background: rgba(0, 0, 0, 0.452);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
background: rgba(0, 0, 0, 0.603);
|
||||
}
|
||||
.input-title{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.form-button{
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.226);
|
||||
display: table;
|
||||
transition: 0.5s;
|
||||
margin: auto;
|
||||
color: white;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-button:hover{
|
||||
background: rgba(0, 0, 0, 0.534)
|
||||
}
|
||||
.alert{
|
||||
color: red;
|
||||
font-size: 15px;
|
||||
width: 220px;
|
||||
}
|
||||
</style>
|
||||
3
vue.config.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
runtimeCompiler: true
|
||||
}
|
||||