add realtime chat sample app with vue (#1885)
3
community/sample-apps/realtime-chat-vue/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
14
community/sample-apps/realtime-chat-vue/.eslintrc.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: ["plugin:vue/essential"],
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off"
|
||||
},
|
||||
parserOptions: {
|
||||
parser: "babel-eslint"
|
||||
}
|
||||
};
|
21
community/sample-apps/realtime-chat-vue/.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*
|
9
community/sample-apps/realtime-chat-vue/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Realtime Chat using GraphQL Subscriptions
|
||||
|
||||
This is the source code for a fully working group chat app that uses subscriptions in Hasura GraphQL Engine. It is built using Vue and Apollo.
|
||||
|
||||
- [Fully working app](https://realtime-chat-vue.hasura.app/)
|
||||
- [Backend](https://realtime-chat.demo.hasura.app/console)
|
||||
|
||||
- For a tutorial how to do that check out this [blog post](https://dev.to/hasurahq/realtime-chat-app-with-vue-and-hasura-202h)
|
||||
- For Vue and GraphQL course check out these [video series](https://dev.to/hasurahq/vue-and-graphql-with-hasura-video-course-3mpp)
|
3
community/sample-apps/realtime-chat-vue/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ["@vue/app"]
|
||||
};
|
11213
community/sample-apps/realtime-chat-vue/package-lock.json
generated
Normal file
37
community/sample-apps/realtime-chat-vue/package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "chat-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"apollo-cache-inmemory": "^1.5.1",
|
||||
"apollo-client": "^2.5.1",
|
||||
"apollo-link": "^1.2.11",
|
||||
"apollo-link-http": "^1.5.14",
|
||||
"apollo-link-ws": "^1.0.17",
|
||||
"apollo-utilities": "^1.2.1",
|
||||
"moment": "^2.24.0",
|
||||
"subscriptions-transport-ws": "^0.9.16",
|
||||
"vue": "^2.6.6",
|
||||
"vue-apollo": "^3.0.0-beta.28",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.5.0",
|
||||
"@vue/cli-plugin-eslint": "^3.5.0",
|
||||
"@vue/cli-service": "^3.5.0",
|
||||
"@vue/eslint-config-prettier": "^4.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"css-loader": "^2.1.1",
|
||||
"eslint": "^5.8.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"graphql-tag": "^2.9.0",
|
||||
"vue-cli-plugin-apollo": "^0.19.2",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
BIN
community/sample-apps/realtime-chat-vue/public/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
20
community/sample-apps/realtime-chat-vue/public/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!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 rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">
|
||||
<link href="https://use.fontawesome.com/releases/v5.0.7/css/all.css" rel="stylesheet" crossorigin="anonymous">
|
||||
<title>chat-app</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but chat-app 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>
|
671
community/sample-apps/realtime-chat-vue/src/App.css
Normal file
@ -0,0 +1,671 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Raleway:400,600');
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400');
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: 'Open Sans';
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.noPadd
|
||||
{
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.removePaddLeft {
|
||||
padding-left: 0;
|
||||
}
|
||||
.addPaddTop
|
||||
{
|
||||
padding-top: 10px;
|
||||
clear: both;
|
||||
}
|
||||
/* Landing section */
|
||||
.wd10 {
|
||||
width: 10%;
|
||||
display: inline-block;
|
||||
}
|
||||
.wd90 {
|
||||
width: 90%;
|
||||
display: inline-block;
|
||||
}
|
||||
.removePaddBottom {
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
.gradientBgColor {
|
||||
background-color: #a0b4cc;
|
||||
/* Safari 4-5, Chrome 1-9 */
|
||||
background: -webkit-gradient(
|
||||
linear,
|
||||
0% 0%,
|
||||
0% 100%,
|
||||
from(#a0b4cc),
|
||||
to(#c2a899)
|
||||
);
|
||||
/* Safari 5.1, Chrome 10+ */
|
||||
background: -webkit-linear-gradient(top, #a0b4cc, #c2a899);
|
||||
/* Firefox 3.6+ */
|
||||
background: -moz-linear-gradient(top, #a0b4cc, #c2a899);
|
||||
/* IE 10 */
|
||||
background: -ms-linear-gradient(top, #a0b4cc, #c2a899);
|
||||
/* Opera 11.10+ */
|
||||
background: -o-linear-gradient(top, #a0b4cc, #c2a899);
|
||||
}
|
||||
.bgImage
|
||||
{
|
||||
position: fixed;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-image: url('./images/chat-app-bg.jpg');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.bgImage::before
|
||||
{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-image: linear-gradient(to bottom right,#000,#a92101);
|
||||
opacity: .9;
|
||||
}
|
||||
.minHeight {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.headerWrapper {
|
||||
padding: 30px 0;
|
||||
padding-left: 75px;
|
||||
min-height: 15vh
|
||||
}
|
||||
.headerDescription {
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
font-family: "Raleway";
|
||||
font-weight: 700;
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
}
|
||||
.headerDescription a {
|
||||
color: #fff;
|
||||
}
|
||||
.headerDescription a:hover {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
.loginBtn {
|
||||
text-align: right;
|
||||
padding-right: 75px;
|
||||
}
|
||||
.loginBtn button {
|
||||
background-color: #f93c18;
|
||||
padding: 10px 30px;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
font-family: "raleway";
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
border-radius: 25px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.loginBtn button:hover {
|
||||
background-color: #e0270e;
|
||||
}
|
||||
.loginBtn button:focus {
|
||||
outline: none;
|
||||
}
|
||||
.mainWrapper {
|
||||
padding-left: 75px;
|
||||
width: 100%;
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.description {
|
||||
font-size: 15px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.appstackWrapper {
|
||||
margin-top: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
padding: 30px;
|
||||
-webkit-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
float: left;
|
||||
color: #606060;
|
||||
}
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: -100px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
.arrow img {
|
||||
width: 120px;
|
||||
display: inline-block;
|
||||
}
|
||||
.appStack {
|
||||
width: 100%;
|
||||
float: left;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.appStack i {
|
||||
font-size: 16px;
|
||||
}
|
||||
.checkBox
|
||||
{
|
||||
color: #00bc00;
|
||||
font-size: 22px !important;
|
||||
}
|
||||
.appStackIconWrapper {
|
||||
width: 100%;
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.appStackIcon img {
|
||||
width: 70%;
|
||||
}
|
||||
.formGroupWrapper
|
||||
{
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
float: left;
|
||||
}
|
||||
.inputGroup
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
.inputGroup input
|
||||
{
|
||||
width: 68% !important;
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
}
|
||||
.inputGroup .groupAppend
|
||||
{
|
||||
width: 32% !important;
|
||||
display: inline-block;
|
||||
}
|
||||
.groupAppend button
|
||||
{
|
||||
width: 100%;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border: 0;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
background-color: #f93c18;
|
||||
color: #fff;
|
||||
font-family: "raleway";
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.groupAppend button:hover
|
||||
{
|
||||
color: #fff;
|
||||
background-color: #e0270e;
|
||||
}
|
||||
.groupAppend button:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
.footer {
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
.footer a {
|
||||
color: #fff;
|
||||
}
|
||||
.footer a:hover {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
.footer i {
|
||||
color: #ed2908;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.tutorialImg {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
.tutorialImg img {
|
||||
width: 95%;
|
||||
display: inline-block;
|
||||
}
|
||||
/* Landing section */
|
||||
.app {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.messageOdd {
|
||||
font-size: 18px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 16px;
|
||||
background-color: #fff;
|
||||
padding-left: 5px;
|
||||
margin: 20px;
|
||||
border-radius: 5px;
|
||||
width: 50%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.selfMessage {
|
||||
font-size: 16px;
|
||||
background-color: #eee;
|
||||
padding-left: 5px;
|
||||
margin: 20px;
|
||||
border-radius: 5px;
|
||||
width: 50%;
|
||||
padding: 5px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.newMessageEven {
|
||||
font-size: 18px;
|
||||
background-color: #98FB98;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.newMessageOdd {
|
||||
font-size: 18px;
|
||||
background-color: #8FBC8F;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.messageWrapperNew {
|
||||
padding-bottom: 75px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: flex-start;
|
||||
background-color: #20c40f;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
font-family: 'raleway';
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.oldNewSeparator {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#chatbox {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 90px);
|
||||
background-color: #f8f9f9;
|
||||
}
|
||||
|
||||
.textboxWrapper {
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 87px;
|
||||
width: 75%;
|
||||
background-color: #fff;
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.textbox {}
|
||||
|
||||
.sendButton {
|
||||
width: 20%;
|
||||
background-color:'green';
|
||||
}
|
||||
|
||||
.login {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loginTextbox {
|
||||
font-size: 16px;
|
||||
height: 43px;
|
||||
width: 74%;
|
||||
margin-right: 1%;
|
||||
font-weight: 300;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
padding-left: 10px;
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.typoTextbox {
|
||||
font-size: 16px;
|
||||
height: 43px;
|
||||
width: 75%;
|
||||
margin-right: 1%;
|
||||
font-weight: 300;
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
padding-left: 10px;
|
||||
display: inline-block;
|
||||
background-color: #f6f6f7;
|
||||
}
|
||||
|
||||
.loginTextbox:focus, .typoTextbox:focus {
|
||||
outline: none;
|
||||
border-color: #016d95;
|
||||
}
|
||||
|
||||
.typoTextbox:focus {
|
||||
outline: none;
|
||||
border-color: #bbbdbd;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
height: 45px;
|
||||
width: 21%;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
background-color: 'green' !important;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-right: 1%;
|
||||
}
|
||||
|
||||
.typoButton {
|
||||
height: 45px;
|
||||
width: 20%;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
background-color: #ffca27;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-right: 1%;
|
||||
border: 0;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.typoButton:hover {
|
||||
background-color: #dba203;
|
||||
}
|
||||
|
||||
.loginButton:focus, .typoButton:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.loginButton:hover {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.loginHeading {
|
||||
text-align: center;
|
||||
font-family: 'Raleway';
|
||||
margin-top: 0;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.loginWrapper {
|
||||
width: 450px;
|
||||
padding: 30px;
|
||||
margin: 0 auto;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid #ececec;
|
||||
border-radius: 5px;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
text-align: center;
|
||||
color: 'red';
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.wd25 {
|
||||
width: 25%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wd75 {
|
||||
width: 75%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.onlineUsers {
|
||||
background-color: #4f5050;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.messageName, .messsageTime {
|
||||
width: 49%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.messageName {
|
||||
color: #1d5d01;
|
||||
}
|
||||
|
||||
.messsageTime {
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
font-size: 12px;
|
||||
color: #01999b;
|
||||
}
|
||||
|
||||
.userList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
-webkit-padding-start: 0px;
|
||||
}
|
||||
|
||||
.userList li {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chatWrapper {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.userListHeading {
|
||||
font-weight: 600;
|
||||
padding: 15px 10px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
background-color: #222;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.typingIndicator {
|
||||
text-align: left;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
.displayFlex
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hasura-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hasura-logo a {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.hasura-logo a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.hasura-logo img {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.App-header {
|
||||
background-color: #222;
|
||||
height: 50px;
|
||||
color: white;
|
||||
text-align: left;
|
||||
}
|
||||
.App-footer{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.footer-small-text{
|
||||
font-size: 12px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.headerDescription
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
.headerWrapper {
|
||||
display: block !important;
|
||||
}
|
||||
.000scription {
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.loginBtn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.wd75
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
.message
|
||||
{
|
||||
width: 90%;
|
||||
}
|
||||
.textboxWrapper
|
||||
{
|
||||
width: 100%;
|
||||
}
|
||||
.mobileview
|
||||
{
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 152px;
|
||||
width: 50%;
|
||||
}
|
||||
.mobileuserListHeading
|
||||
{
|
||||
font-size: 14px;
|
||||
background-color: #222;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
.mobileuserListHeading i
|
||||
{
|
||||
margin-left: 10px;
|
||||
}
|
||||
.mobileUserList
|
||||
{
|
||||
background-color: #4f5050;
|
||||
padding-inline-start: 0px;
|
||||
-webkit-padding-start: 0px;
|
||||
-moz-padding-start: 0px;
|
||||
-o-padding-start: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.mobileUserList li
|
||||
{
|
||||
list-style-type: none;
|
||||
color: #fff;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
.hasura-logo a {
|
||||
padding: 0 0px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.headerWrapper {
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
height: auto;
|
||||
}
|
||||
.mainWrapper {
|
||||
min-height: auto;
|
||||
padding-left: 0;
|
||||
height: auto;
|
||||
}
|
||||
.minHeight {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.loginBtn {
|
||||
padding-right: 0;
|
||||
}
|
||||
.appstackWrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
.appStack {
|
||||
display: flex;
|
||||
}
|
||||
.flexWidth {
|
||||
flex: 1;
|
||||
}
|
||||
.description {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.appStackIconWrapper {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.inputGroup input
|
||||
{
|
||||
width: 63% !important;
|
||||
}
|
||||
.inputGroup .groupAppend
|
||||
{
|
||||
width: 37% !important;
|
||||
}
|
||||
.groupAppend button {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
}
|
7
community/sample-apps/realtime-chat-vue/src/App.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div id="app" class="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
BIN
community/sample-apps/realtime-chat-vue/src/assets/logo.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="apollo-example">
|
||||
<!-- Cute tiny form -->
|
||||
<div class="form">
|
||||
<label for="field-name" class="label">Name</label>
|
||||
<input
|
||||
v-model="name"
|
||||
placeholder="Type a name"
|
||||
class="input"
|
||||
id="field-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Apollo watched Graphql query -->
|
||||
<ApolloQuery
|
||||
:query="require('../graphql/HelloWorld.gql')"
|
||||
:variables="{ name }"
|
||||
>
|
||||
<template slot-scope="{ result: { loading, error, data } }">
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="loading apollo">Loading...</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-else-if="error" class="error apollo">An error occured</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-else-if="data" class="result apollo">{{ data.hello }}</div>
|
||||
|
||||
<!-- No result -->
|
||||
<div v-else class="no-result apollo">No result :(</div>
|
||||
</template>
|
||||
</ApolloQuery>
|
||||
|
||||
<!-- Tchat example -->
|
||||
<ApolloQuery :query="require('../graphql/Messages.gql')">
|
||||
<ApolloSubscribeToMore
|
||||
:document="require('../graphql/MessageAdded.gql')"
|
||||
:update-query="onMessageAdded"
|
||||
/>
|
||||
|
||||
<div slot-scope="{ result: { data } }">
|
||||
<template v-if="data">
|
||||
<div
|
||||
v-for="message of data.messages"
|
||||
:key="message.id"
|
||||
class="message"
|
||||
>
|
||||
{{ message.text }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ApolloQuery>
|
||||
|
||||
<ApolloMutation
|
||||
:mutation="require('../graphql/AddMessage.gql')"
|
||||
:variables="{
|
||||
input: {
|
||||
text: newMessage
|
||||
}
|
||||
}"
|
||||
class="form"
|
||||
@done="newMessage = ''"
|
||||
>
|
||||
<template slot-scope="{ mutate }">
|
||||
<form v-on:submit.prevent="formValid && mutate()">
|
||||
<label for="field-message">Message</label>
|
||||
<input
|
||||
id="field-message"
|
||||
v-model="newMessage"
|
||||
placeholder="Type a message"
|
||||
class="input"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
</ApolloMutation>
|
||||
|
||||
<div class="images">
|
||||
<div v-for="file of files" :key="file.id" class="image-item">
|
||||
<img :src="`${$filesRoot}/${file.path}`" class="image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-input">
|
||||
<label for="field-image">Image</label>
|
||||
<input
|
||||
id="field-image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
required
|
||||
@change="onUploadImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FILES from "../graphql/Files.gql";
|
||||
import UPLOAD_FILE from "../graphql/UploadFile.gql";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
name: "Anne",
|
||||
newMessage: ""
|
||||
};
|
||||
},
|
||||
|
||||
apollo: {
|
||||
files: FILES
|
||||
},
|
||||
|
||||
computed: {
|
||||
formValid() {
|
||||
return this.newMessage;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onMessageAdded(previousResult, { subscriptionData }) {
|
||||
return {
|
||||
messages: [
|
||||
...previousResult.messages,
|
||||
subscriptionData.data.messageAdded
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
async onUploadImage({ target }) {
|
||||
if (!target.validity.valid) return;
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPLOAD_FILE,
|
||||
variables: {
|
||||
file: target.files[0]
|
||||
},
|
||||
update: (store, { data: { singleUpload } }) => {
|
||||
const data = store.readQuery({ query: FILES });
|
||||
data.files.push(singleUpload);
|
||||
store.writeQuery({ query: FILES, data });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form,
|
||||
.input,
|
||||
.apollo,
|
||||
.message {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: solid 2px #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 300px);
|
||||
grid-auto-rows: 300px;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ccc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.image-input {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div className="banner" @click="bannerClick">
|
||||
You have {{numOfNewMessages}} new message(s)
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Banner",
|
||||
props: ["numOfNewMessages"],
|
||||
methods: {
|
||||
bannerClick(){
|
||||
this.$emit("bannerClicked")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="chatWrapper">
|
||||
<div class="wd25 hidden-xs">
|
||||
<OnlineUsers
|
||||
:userId="userId"
|
||||
:username="username"
|
||||
/>
|
||||
</div>
|
||||
<div class="mobileview visible-xs">
|
||||
<OnlineUsers
|
||||
:userId="userId"
|
||||
:username="username"
|
||||
:isMobileView="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="wd75">
|
||||
<RenderMessages
|
||||
:username="username"
|
||||
:userId="userId"
|
||||
/>
|
||||
<Textbox
|
||||
:username="username"
|
||||
:userId="userId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OnlineUsers from "@/components/OnlineUsers.vue";
|
||||
import RenderMessages from "@/components/RenderMessages.vue";
|
||||
import Textbox from "@/components/TextBox.vue";
|
||||
|
||||
export default {
|
||||
name: "ChatWrapper",
|
||||
components: {
|
||||
OnlineUsers,
|
||||
RenderMessages,
|
||||
Textbox
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br />
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-cli documentation</a
|
||||
>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>babel</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>eslint</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
|
||||
>Forum</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
|
||||
>Community Chat</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
|
||||
>Twitter</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-router</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/vue-devtools#vue-devtools"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>vue-devtools</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
|
||||
>vue-loader</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/vuejs/awesome-vue"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>awesome-vue</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "HelloWorld",
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div v-bind:class="listClassName">
|
||||
<div class="message" v-for="(m, key) in messages" v-bind:key="key">
|
||||
<div class="messageNameTime">
|
||||
<div class="messageName">
|
||||
<b>{{m.username}}</b>
|
||||
</div>
|
||||
<div class="messsageTime">
|
||||
<i>{{getMessageTime(m.timestamp)}} </i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messageText">
|
||||
{{m.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="lastMessage"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: "MessageList",
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
default(){
|
||||
return []
|
||||
}
|
||||
},
|
||||
isNew: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
username: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getMessageTime(timestamp){
|
||||
return moment(timestamp).fromNow()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listClassName(){
|
||||
return this.isNew ? "messageWrapperNew" : "messageWrapper"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#lastMessage {
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div :class="[isMobileView ? 'mobileonlineUsers visible-xs' : 'onlineUsers hidden-xs']">
|
||||
<div>
|
||||
<p
|
||||
:class="[isMobileView ? 'mobileuserListHeading' : 'userListHeading']"
|
||||
@click="toggleMobileView"
|
||||
>
|
||||
Online Users ({{!usersOnline ? 0 : usersOnline.length}})
|
||||
<i v-if="isMobileView" :class="[showMobileView ? 'fa fa-angle-down': 'fa fa-angle-up']"></i>
|
||||
</p>
|
||||
<ul v-if="(isMobileView && this.showMobileView) || !isMobileView" :class="[isMobileView ? 'mobileUserList' :'userList']">
|
||||
<li v-for="(user, key) in usersOnline" v-bind:key="key">
|
||||
{{user.username}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "OnlineUsers",
|
||||
data(){
|
||||
return {
|
||||
usersOnline: [],
|
||||
text: '',
|
||||
showMobileView: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMobileView(){
|
||||
this.showMobileView = !this.showMobileView;
|
||||
}
|
||||
},
|
||||
props: ["userId", "isMobileView"],
|
||||
apollo: {
|
||||
$subscribe: {
|
||||
users: {
|
||||
query: require('../graphql/fetchOnlineUsersSubscription.gql'),
|
||||
result({ data, loading, error}) {
|
||||
if (error) {
|
||||
this.text = "Error loading online"
|
||||
}
|
||||
if (loading) {
|
||||
this.text = "Loading..."
|
||||
}
|
||||
this.usersOnline = data.user_online
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div id="chatbox">
|
||||
<MessageList :messages="messages" username="vnovick"/>
|
||||
<div class="bottom"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Banner from './Banner'
|
||||
import MessageList from './MessageList'
|
||||
|
||||
export default {
|
||||
name: "RenderMessages",
|
||||
components: {
|
||||
Banner,
|
||||
MessageList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
newMessages: [],
|
||||
bottom: false
|
||||
}
|
||||
},
|
||||
created(){
|
||||
window.addEventListener("scroll", this.handleScroll);
|
||||
},
|
||||
updated(){
|
||||
this.scrollToBottom();
|
||||
},
|
||||
methods: {
|
||||
handleBannerClick(){
|
||||
alert("Banner Clicked")
|
||||
},
|
||||
scrollToBottom() {
|
||||
document.getElementById('lastMessage').scrollIntoView({ behavior: "instant" });
|
||||
},
|
||||
handleScroll(e) {
|
||||
const windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
|
||||
const body = document.getElementById("chatbox");
|
||||
const html = document.documentElement;
|
||||
const docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
|
||||
const windowBottom = windowHeight + window.pageYOffset;
|
||||
if (windowBottom >= docHeight) {
|
||||
this.bottom = true
|
||||
} else {
|
||||
if (this.bottom) {
|
||||
this.bottom = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
messages: {
|
||||
query: require('../graphql/fetchMessages.gql'),
|
||||
loadingKey: "loading",
|
||||
variables(){
|
||||
return {
|
||||
last_received_id: -1,
|
||||
last_received_ts: "2018-08-21T19:58:46.987552+00:00"
|
||||
}
|
||||
},
|
||||
update(data){
|
||||
const receivedmessages = data.message
|
||||
return receivedmessages
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
subscribeToMore: {
|
||||
document: require('../graphql/subscribeToNewMessages.gql'),
|
||||
updateQuery: (previousResult, { subscriptionData }) => {
|
||||
if (previousResult) {
|
||||
return {
|
||||
message: [
|
||||
...previousResult.message,
|
||||
...subscriptionData.data.message
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
error(){
|
||||
alert("Error occured")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bottom {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="sendMessage">
|
||||
<div class="textboxWrapper">
|
||||
<TypingIndicator :userId="userId" :username="username"/>
|
||||
<input
|
||||
id="textbox"
|
||||
class="textbox typoTextbox"
|
||||
v-model="text"
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
class="sendButton typoButton"
|
||||
@click.prevent="sendMessage"
|
||||
> Send </button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TypingIndicator from './TypingIndicator'
|
||||
export default {
|
||||
name: "Textbox",
|
||||
props: ["userId","username"],
|
||||
data(){
|
||||
return {
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
TypingIndicator
|
||||
},
|
||||
watch: {
|
||||
text: function(value){
|
||||
const textLength = value.length;
|
||||
if ((textLength !== 0 && textLength % 5 === 0) || textLength === 1) {
|
||||
this.emitTypingEvent();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitTypingEvent(){
|
||||
if(this.userId) {
|
||||
this.$apollo.mutate({
|
||||
mutation: require('../graphql/emitTypingEvent.gql'),
|
||||
variables: {
|
||||
userId: this.userId
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
sendMessage(event){
|
||||
if (this.text === '') return
|
||||
this.$apollo.mutate({
|
||||
mutation: require('../graphql/insertMessage.gql'),
|
||||
variables: {
|
||||
message: {
|
||||
username: this.username,
|
||||
text: this.text
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="typingIndicator">
|
||||
{{ text }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TypingIndicator",
|
||||
data(){
|
||||
return {
|
||||
text: ''
|
||||
}
|
||||
},
|
||||
props: ["userId", "username"],
|
||||
apollo: {
|
||||
$subscribe: {
|
||||
userTyping: {
|
||||
query: require('../graphql/getUserTyping.gql'),
|
||||
variables() {
|
||||
return {
|
||||
selfId: this.userId
|
||||
}
|
||||
},
|
||||
result({ data, loading, error }) {
|
||||
if (data.user_typing.length !== 0) {
|
||||
this.text = `${data.user_typing[0].username} is typing...`
|
||||
} else {
|
||||
this.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -0,0 +1,12 @@
|
||||
mutation ($username: String!) {
|
||||
insert_user (
|
||||
objects: [{
|
||||
username: $username
|
||||
}]
|
||||
) {
|
||||
returning {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
mutation ($userId:Int!){
|
||||
update_user (
|
||||
_set: {
|
||||
last_seen: "now()"
|
||||
}
|
||||
where: {
|
||||
id: {
|
||||
_eq: $userId
|
||||
}
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
mutation ($userId: Int) {
|
||||
update_user (
|
||||
_set: {
|
||||
last_typed: "now()"
|
||||
}
|
||||
where: {
|
||||
id: {
|
||||
_eq: $userId
|
||||
}
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
query ($last_received_id: Int, $last_received_ts: timestamptz){
|
||||
message (
|
||||
order_by: {timestamp:asc}
|
||||
where: {
|
||||
_and: {
|
||||
id: {
|
||||
_neq: $last_received_id
|
||||
},
|
||||
timestamp: {
|
||||
_gte: $last_received_ts
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
id
|
||||
text
|
||||
username
|
||||
timestamp
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
subscription {
|
||||
user_online (
|
||||
order_by: {username:asc}
|
||||
) {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
subscription ($selfId: Int ) {
|
||||
user_typing (
|
||||
where: {
|
||||
id: {
|
||||
_neq: $selfId
|
||||
}
|
||||
},
|
||||
limit: 1
|
||||
order_by: {last_typed:desc}
|
||||
){
|
||||
last_typed
|
||||
username
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
mutation insert_message ($message: message_insert_input! ){
|
||||
insert_message (
|
||||
objects: [$message]
|
||||
) {
|
||||
returning {
|
||||
id
|
||||
timestamp
|
||||
text
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
subscription {
|
||||
message ( order_by: {id:desc} limit: 1) {
|
||||
id
|
||||
username
|
||||
text
|
||||
timestamp
|
||||
}
|
||||
}
|
BIN
community/sample-apps/realtime-chat-vue/src/images/Vue-logo.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
community/sample-apps/realtime-chat-vue/src/images/apollo.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
community/sample-apps/realtime-chat-vue/src/images/auth.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 444 KiB |
BIN
community/sample-apps/realtime-chat-vue/src/images/chat-app.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
community/sample-apps/realtime-chat-vue/src/images/graphql.png
Normal file
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 220 80" style="enable-background:new 0 0 220 80;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#102954;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M67.4,26.8c2.1-5.2,2.2-16-0.7-24.4l0,0c-0.7-1.5-3-1.1-3.1,0.7v0.6c-0.5,7.9-3.4,12.2-7.6,14.2
|
||||
c-0.7,0.3-1.8,0.2-2.5-0.2c-5.1-3.2-11-5.1-17.5-5.1s-12.4,1.9-17.5,5.1c-0.7,0.4-1.5,0.5-2.2,0.2C12,16.3,8.9,11.5,8.4,3.6V3.1
|
||||
c0-1.6-2.3-2.1-3.1-0.7c-3,8.3-2.9,19.1-0.7,24.4c1.1,2.6,1.1,5.6,0.2,8.3c-1.2,3.4-1.8,7.2-1.7,11c0.3,17.4,15.1,32.2,32.4,32.4
|
||||
c18.3,0.2,33.3-14.6,33.3-32.9c0-3.7-0.6-7.2-1.7-10.5C66.3,32.4,66.4,29.4,67.4,26.8z"/>
|
||||
</g>
|
||||
<ellipse class="st1" cx="36" cy="45.5" rx="25" ry="25"/>
|
||||
<path class="st0" d="M39.9,42.9L34,33.8c-1-1.5-2.9-1.9-4.4-1c-0.9,0.6-1.5,1.6-1.5,2.7c0,0.6,0.2,1.2,0.6,1.7l4,6.2
|
||||
c0.3,0.5,0.2,1.1-0.1,1.5l-6.2,6.8c-1.1,1.3-1.1,3.3,0.2,4.5c0.6,0.6,1.4,0.8,2.2,0.8c0.9,0,1.7-0.4,2.3-1.1l4.6-5.4
|
||||
c0.3-0.4,1-0.4,1.3,0.1l3.3,4.7c0.2,0.3,0.5,0.7,0.9,1c1.1,0.8,2.5,0.7,3.5,0.1l0,0c0.9-0.6,1.5-1.6,1.5-2.7
|
||||
c0-0.6-0.2-1.2-0.5-1.7L39.9,42.9z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M96.8,45.6h-3.9c-0.6,0-1.1-0.5-1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.4c-0.6,0-1.1,0.5-1.1,1.1v31.3
|
||||
c0,0.6,0.5,1.1,1.1,1.1h3.4c0.6,0,1.1-0.5,1.1-1.1V51.3c0-0.6,0.5-1.1,1.1-1.1h3.9c0.6,0,1.1,0.5,1.1,1.1v12.2
|
||||
c0,0.6,0.5,1.1,1.1,1.1h3.4c0.6,0,1.1-0.5,1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1H99c-0.6,0-1.1,0.5-1.1,1.1v12.3
|
||||
C97.9,45.1,97.4,45.6,96.8,45.6z"/>
|
||||
<path class="st0" d="M114.2,32l-5.5,31.3c-0.1,0.6,0.4,1.2,1,1.2h3.4c0.5,0,1-0.4,1-0.9l0.9-5.7c0.1-0.5,0.5-0.9,1-0.9h4.3
|
||||
c0.5,0,1,0.4,1,0.9l1,5.8c0.1,0.5,0.5,0.9,1,0.9h3.5c0.7,0,1.2-0.6,1-1.3L122,32c-0.1-0.5-0.5-0.9-1-0.9h-5.8
|
||||
C114.7,31.1,114.3,31.5,114.2,32z M119.3,52.3h-2.2c-0.7,0-1.1-0.6-1-1.2l1-11.5c0.2-1.2,1.9-1.2,2.1,0l1.1,11.5
|
||||
C120.4,51.7,119.9,52.3,119.3,52.3z"/>
|
||||
<path class="st0" d="M143,45.2h-3.8c-0.7,0-1.1-0.3-1.1-1.1v-7.2c0-0.7,0.4-1.1,1.1-1.1h2.2c0.7,0,1.1,0.3,1.1,1.1v3.4
|
||||
c0,0.6,0.5,1.1,1.1,1.1h3.5c0.6,0,1.1-0.5,1.1-1.1v-4.2c0-3.3-1.8-5-5.3-5h-5.1c-3.5,0-5.3,1.7-5.3,5v8.6c0,3.3,1.8,5.1,5.2,5.1
|
||||
h3.8c0.7,0,1.1,0.3,1.1,1.1v8c0,0.7-0.3,1.1-1.1,1.1h-2.2c-0.7,0-1.1-0.3-1.1-1.1v-3.4c0-0.6-0.5-1.1-1.1-1.1h-3.5
|
||||
c-0.6,0-1.1,0.5-1.1,1.1v4.2c0,3.3,1.8,5,5.3,5h5c3.5,0,5.3-1.7,5.3-5v-9.4C148.2,46.9,146.4,45.2,143,45.2z"/>
|
||||
<path class="st0" d="M164,58.8c0,0.7-0.3,1.1-1.1,1.1h-3c-0.7,0-1.1-0.3-1.1-1.1V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.5
|
||||
c-0.6,0-1.1,0.5-1.1,1.1v27.3c0,3.3,1.8,5,5.3,5h5.8c3.5,0,5.3-1.7,5.3-5V32.2c0-0.6-0.5-1.1-1.1-1.1h-3.3c-0.6,0-1.1,0.5-1.1,1.1
|
||||
L164,58.8L164,58.8z"/>
|
||||
<path class="st0" d="M191.8,46.3V36.1c0-3.3-1.8-5-5.3-5h-10c-0.6,0-1.1,0.5-1.1,1.1v31.3c0,0.6,0.5,1.1,1.1,1.1h3.4
|
||||
c0.6,0,1.1-0.5,1.1-1.1v-11c0-0.6,0.5-1.1,1.1-1.1l0,0c0.4,0,0.8,0.3,1,0.7l4.4,11.8c0.2,0.4,0.6,0.7,1,0.7h3.7
|
||||
c0.7,0,1.3-0.7,1-1.4L189,52.3c-0.2-0.5,0.1-1.1,0.6-1.4C190.9,50.1,191.8,48.5,191.8,46.3z M186.2,36.9v8.8
|
||||
c0,0.7-0.4,1.1-1.1,1.1H182c-0.6,0-1.1-0.5-1.1-1.1v-8.8c0-0.6,0.5-1.1,1.1-1.1h3.1C185.8,35.8,186.2,36.2,186.2,36.9z"/>
|
||||
<path class="st0" d="M210.2,31.1h-5.8c-0.5,0-1,0.4-1,0.9l-5.5,31.3c-0.1,0.6,0.4,1.2,1,1.2h3.4c0.5,0,1-0.4,1-0.9l0.9-5.7
|
||||
c0.1-0.5,0.5-0.9,1-0.9h4.3c0.5,0,1,0.4,1,0.9l1,5.8c0.1,0.5,0.5,0.9,1,0.9h3.5c0.7,0,1.2-0.6,1-1.3L211.2,32
|
||||
C211.1,31.5,210.7,31.1,210.2,31.1z M208.4,52.3h-2.1c-0.7,0-1.1-0.6-1-1.2l1-10.5c0.2-1.2,1.9-1.2,2.1,0l1.1,10.5
|
||||
C209.6,51.7,209.1,52.3,208.4,52.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
15
community/sample-apps/realtime-chat-vue/src/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import style from "./App.css";
|
||||
import { createProvider } from "./vue-apollo";
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
style,
|
||||
store,
|
||||
apolloProvider: createProvider(),
|
||||
render: h => h(App)
|
||||
}).$mount("#app");
|
50
community/sample-apps/realtime-chat-vue/src/router.js
Normal file
@ -0,0 +1,50 @@
|
||||
import Vue from "vue";
|
||||
import Router from "vue-router";
|
||||
import Home from "./views/Home.vue";
|
||||
import Login from "./views/Login.vue";
|
||||
import { TokenService } from "./services/storage";
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const router = new Router({
|
||||
mode: "history",
|
||||
base: process.env.BASE_URL,
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: "/chat/:userId",
|
||||
component: Home,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
name: "login",
|
||||
component: Login,
|
||||
meta: {
|
||||
public: true
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isPublic = to.matched.some(record => record.meta.public);
|
||||
const onlyWhenLoggedOut = to.matched.some(record => record.meta.onlyWhenLoggedOut)
|
||||
const loggedIn = !!TokenService.getToken();
|
||||
if (!isPublic && !loggedIn) {
|
||||
return next({
|
||||
path: "/login",
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
}
|
||||
if (loggedIn && onlyWhenLoggedOut) {
|
||||
return next('/')
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
@ -0,0 +1,34 @@
|
||||
const TOKEN_KEY = "access_token";
|
||||
const REFRESH_TOKEN_KEY = "refresh_token";
|
||||
|
||||
/**
|
||||
* Manage the how Access Tokens are being stored and retreived from storage.
|
||||
*
|
||||
* Current implementation stores to localStorage. Local Storage should always be
|
||||
* accessed through this instace.
|
||||
**/
|
||||
export const TokenService = {
|
||||
getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
},
|
||||
|
||||
saveToken(accessToken) {
|
||||
localStorage.setItem(TOKEN_KEY, accessToken);
|
||||
},
|
||||
|
||||
removeToken() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
},
|
||||
|
||||
getRefreshToken() {
|
||||
return localStorage.getItem(REFRESH_TOKEN_KEY);
|
||||
},
|
||||
|
||||
saveRefreshToken(refreshToken) {
|
||||
localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
|
||||
},
|
||||
|
||||
removeRefreshToken() {
|
||||
localStorage.removeItem(REFRESH_TOKEN_KEY);
|
||||
}
|
||||
};
|
10
community/sample-apps/realtime-chat-vue/src/store.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {},
|
||||
mutations: {},
|
||||
actions: {}
|
||||
});
|
84
community/sample-apps/realtime-chat-vue/src/views/Home.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<ChatWrapper :userId="userId" :username="username"/>
|
||||
<footer class="App-footer">
|
||||
<div class="hasura-logo">
|
||||
<img src="https://graphql-engine-cdn.hasura.io/img/powered_by_hasura_black.svg" @click="windowOpen" alt="Powered by Hasura"/>
|
||||
|
|
||||
<a href="https://realtime-chat.demo.hasura.app/console" target="_blank" rel="noopener noreferrer">
|
||||
Backend
|
||||
</a>
|
||||
|
|
||||
<a href="https://github.com/hasura/graphql-engine/tree/master/community/examples/realtime-chat-vue" target="_blank" rel="noopener noreferrer">
|
||||
Source
|
||||
</a>
|
||||
|
|
||||
<a href="https://dev.to/hasurahq/realtime-chat-app-with-vue-and-hasura-57db" target="_blank" rel="noopener noreferrer">
|
||||
Blogpost
|
||||
</a>
|
||||
<a href="https://dev.to/hasurahq/vue-and-graphql-with-hasura-video-course-3mpp" target="_blank" rel="noopener noreferrer">
|
||||
Vue and GraphQL course
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer-small-text"><span>(The database resets every 24 hours)</span></div>
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="submit"
|
||||
@click="logOut"
|
||||
>
|
||||
Log Out
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
import ChatWrapper from "@/components/ChatWrapper.vue";
|
||||
import { TokenService } from '../services/storage';
|
||||
|
||||
export default {
|
||||
name: "home",
|
||||
components: {
|
||||
ChatWrapper
|
||||
},
|
||||
props:{
|
||||
userId: {
|
||||
type: String,
|
||||
default() {
|
||||
const savedData = JSON.parse(TokenService.getToken())
|
||||
return savedData && `${savedData.userId}`
|
||||
}
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
default(){
|
||||
const savedData = JSON.parse(TokenService.getToken())
|
||||
return this.$route.query.username || savedData && savedData.username
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
windowOpen(){
|
||||
window.open("https://hasura.io")
|
||||
},
|
||||
logOut() {
|
||||
TokenService.removeToken()
|
||||
this.$router.go();
|
||||
}
|
||||
},
|
||||
created(){
|
||||
setInterval(
|
||||
async () => {
|
||||
await this.$apollo.mutate({
|
||||
mutation: require('../graphql/emitOnlineEvent.gql'),
|
||||
variables: {
|
||||
userId: this.userId
|
||||
}
|
||||
})
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
};
|
||||
</script>
|
225
community/sample-apps/realtime-chat-vue/src/views/Login.vue
Normal file
@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<ApolloMutation
|
||||
:mutation="require('../graphql/AddUserMutation.gql')"
|
||||
:variables="{
|
||||
username
|
||||
}"
|
||||
@done="login"
|
||||
>
|
||||
<template slot-scope="{ mutate, loading, error }">
|
||||
<div class="container-fluid minHeight">
|
||||
<div class="bgImage"></div>
|
||||
<div>
|
||||
<div class="headerWrapper">
|
||||
<div class="headerDescription">
|
||||
Realtime Chat App
|
||||
</div>
|
||||
</div>
|
||||
<div class="mainWrapper">
|
||||
<div class="col-md-5 col-sm-6 col-xs-12 noPadd">
|
||||
<div class="appstackWrapper">
|
||||
<div class="appStack">
|
||||
<div class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
|
||||
<i class="em em---1" />
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description">
|
||||
Try out a realtime app that uses
|
||||
</div>
|
||||
<div class="appStackIconWrapper">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4 noPadd">
|
||||
<div class="appStackIcon">
|
||||
<img
|
||||
class="img-responsive"
|
||||
v-bind:src="vueLogo"
|
||||
alt="React logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 col-sm-8 col-xs-8 noPadd">
|
||||
<div class="appStackIcon">
|
||||
<img
|
||||
class="img-responsive"
|
||||
v-bind:src="graphql"
|
||||
alt="GraphQL logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appStack">
|
||||
<div class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
|
||||
<i class="em em-rocket" />
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description">Powered by</div>
|
||||
<div class="appStackIconWrapper">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4 noPadd">
|
||||
<div class="appStackIcon">
|
||||
<img
|
||||
class="img-responsive"
|
||||
v-bind:src="apolloLogo"
|
||||
alt="apollo logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-4 col-xs-4 noPadd">
|
||||
<div class="appStackIcon">
|
||||
<img
|
||||
class="img-responsive"
|
||||
v-bind:src="hasuraLogo"
|
||||
alt="Hasura logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appStack">
|
||||
<div class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
|
||||
<i class="em em-sunglasses" />
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description removePaddBottom">
|
||||
Explore the Hasura GraphQL backend and try out some queries &
|
||||
mutations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appStack removePaddBottom">
|
||||
<div class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth">
|
||||
<i class="fas fa-check-square checkBox"></i>
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description removePaddBottom">
|
||||
What you get...
|
||||
</div>
|
||||
<div class="addPaddTop">
|
||||
<div
|
||||
class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth"
|
||||
>
|
||||
<i class="em em-hammer_and_wrench"></i>
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description removePaddBottom">
|
||||
Source code
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="addPaddTop">
|
||||
<div
|
||||
class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth"
|
||||
>
|
||||
<i class="em em-closed_lock_with_key"></i>
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description removePaddBottom">
|
||||
Access to GraphQL Backend
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="addPaddTop">
|
||||
<div
|
||||
class="col-md-1 col-sm-1 col-xs-2 removePaddLeft flexWidth"
|
||||
>
|
||||
<i class="em em-zap" />
|
||||
</div>
|
||||
<div class="col-md-11 col-sm-11 col-xs-10 noPadd">
|
||||
<div class="description removePaddBottom">
|
||||
Full Tutorial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formGroupWrapper">
|
||||
<div class="input-group inputGroup">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Enter your username"
|
||||
v-model="username"
|
||||
/>
|
||||
<div class="input-group-append groupAppend">
|
||||
<button
|
||||
class="btn btn-outline-secondary"
|
||||
type="submit"
|
||||
v-on:click.prevent="validateUserAndInsert(mutate)"
|
||||
:disabled="loading || username === ''"
|
||||
>
|
||||
{{ loading ? "Please wait ..." : "Get Started" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="error" class="alert alert-danger addPaddTop">
|
||||
{{error && "Error occurred. Probably user already exists"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Built with
|
||||
<i class="fas fa-heart" />
|
||||
by
|
||||
<a
|
||||
href="https://hasura.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Hasura
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tutorialImg col-md-6 col-sm-6 col-xs-12 hidden-xs noPadd">
|
||||
<img class="img-responsive" v-bind:src="rightImg" alt="View" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ApolloMutation>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TokenService } from '../services/storage';
|
||||
const vueLogo = require("../images/Vue-logo.png");
|
||||
const graphql = require("../images/graphql.png");
|
||||
const hasuraLogo = require("../images/green-logo-white.svg");
|
||||
const apolloLogo = require("../images/apollo.png");
|
||||
const rightImg = require("../images/chat-app.png");
|
||||
|
||||
export default {
|
||||
name: "Login",
|
||||
data: () => ({
|
||||
username: "",
|
||||
rightImg,
|
||||
apolloLogo,
|
||||
hasuraLogo,
|
||||
graphql,
|
||||
vueLogo,
|
||||
loading: false
|
||||
}),
|
||||
methods: {
|
||||
validateUserAndInsert(insert_user) {
|
||||
if (this.username.match(/^[a-z0-9_-]{3,15}$/g)) {
|
||||
insert_user()
|
||||
} else {
|
||||
alert(
|
||||
"Invalid username. Spaces and special characters not allowed. Max 15 charachters. Please try again"
|
||||
);
|
||||
this.username = "";
|
||||
}
|
||||
},
|
||||
login({ data }){
|
||||
const userId = data.insert_user.returning[0].id
|
||||
TokenService.saveToken(JSON.stringify({
|
||||
userId,
|
||||
username: this.username
|
||||
}))
|
||||
this.$router.push({ path: `/chat/${userId}`, query: {
|
||||
username: this.username
|
||||
} })
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
51
community/sample-apps/realtime-chat-vue/src/vue-apollo.js
Normal file
@ -0,0 +1,51 @@
|
||||
import Vue from "vue";
|
||||
|
||||
import { ApolloClient } from "apollo-client";
|
||||
import { HttpLink } from "apollo-link-http";
|
||||
import { InMemoryCache } from "apollo-cache-inmemory";
|
||||
|
||||
import { WebSocketLink } from "apollo-link-ws";
|
||||
import { getMainDefinition } from "apollo-utilities";
|
||||
import { split } from "apollo-link";
|
||||
|
||||
|
||||
import VueApollo from "vue-apollo";
|
||||
// Http endpoint
|
||||
const httpLink = new HttpLink({
|
||||
uri: "https://realtime-chat.demo.hasura.app/v1alpha1/graphql"
|
||||
})
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: "wss://realtime-chat.demo.hasura.app/v1alpha1/graphql",
|
||||
options: {
|
||||
reconnect: true
|
||||
}
|
||||
});
|
||||
|
||||
const link = split(
|
||||
({ query }) => {
|
||||
const { kind, operation } = getMainDefinition(query);
|
||||
return kind === "OperationDefinition" && operation === "subscription";
|
||||
},
|
||||
wsLink,
|
||||
httpLink
|
||||
);
|
||||
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
link,
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: true
|
||||
});
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
// Call this in the Vue app file
|
||||
export function createProvider() {
|
||||
return new VueApollo({
|
||||
defaultClient: apolloClient,
|
||||
defaultOptions: {
|
||||
$loadingKey: "loading"
|
||||
}
|
||||
});
|
||||
}
|