Full-Stack Vue with GraphQL - The Ultimate Guide
Github Repositories
The Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course explains how to Build a complete Pinterest-inspired full-stack app from scratch with Vue, GraphQL, Apollo 2, Vuex, and Vuetify.
Table of contents
- What I've learned
- Section 1: Introduction 0 / 3|29min
- Section 2: What is GraphQL / Apollo? (Optional) 0 / 2|23min
- Section 3: Intro to Apollo Server 2, Queries, Mutations and GraphQL Playground 0 / 5|27min
- Section 4: Connect to MLab Database, Create Mongoose Models and GraphQL TypeDefs 0 / 7|54min
- 11. Create MongoDB Atlas Database, Connect to GraphQL Server 7min
- 12. Update! Connecting to MongoDB Atlas instead of MLab 4min
- 13. Creating Mongoose Schemas 11min
- 14. Creating typeDefs for Project 8min
- 15. Write and Run signupUser Mutation 9min
- 16. Write and Run addPost Mutation 7min
- 17. Write and Run getPosts Query, Intro to populate 7min
- Section 5: Create Vue Frontend with Vue CLI 3 0 / 8|57min
- 18. Create Vue Client with Vue-CLI 3 8min
- 19. Adding Plugins with Vue GUI and Concurrently Dev Script 4min
- 20. Structuring our Vue App 4min
- 21. Installing Vuetify Plugin and Generating a Theme 10min
- 22. Coolors.co for Creating Great Color Schemes (Optional) 4min
- 23. Horizontal Navbar and Mobile First Design 12min
- 24. Add Side Navbar 7min
- 25. Add Routing and Page Transitions 9min
- Section 6: Using Vue Apollo 0 / 4|24min
- Section 7: Integrate Vuex with ApolloClient 0 / 4|23min
- Section 8: JWT Authentication for Signin / Signup 0 / 12|1hr 27min
- 34. Create Gravatar Avatar and Hash User Passwords on Signup 7min
- 35. Write and Run signinUser Mutation 5min
- 36. Sign Token and Return it Upon Signin/Signup 8min
- 37. Using Variables in GraphQL, Signin / Signup Mutation Defs 6min
- 38. Add Signin Form, Write and Run signinUser Action, Return JWT 9min
- 39. Additional Config for ApolloClient, Send Token from LocalStorage 8min
- 40. Verify JWT Token in server.js, Pass Result to currentUser in Context 7min
- 41. Create getCurrentUser Query, Execute it from main.js 9min
- 42. Redirect Home upon Signin with Watcher 8min
- 43. Change Navbar for Signed-in User 9min
- 44. Create Signout Action 6min
- 45. Protected Routes and Clearing Malformed Tokens 5min
- Section 9: Error Handling and Form Validation 0 / 6|43min
- 46. Adding Global Form Alert Component 9min
- 47. Add Loading Spinner to Signin Button 3min
- 48. Form Validation with Vuetify in Signin Component 7min
- 49. Show AuthSnackbar on Signin / Signup 5min
- 50. Handle Authentication Errors, Show Auth Error Snackbar 9min
- 51. Create Signup Form and Signup User Action 11min
- Section 10: Add Post / Infinite Scroll Components 0 / 6|58min
- 52. Make Add Post Form 8min
- 53. Create and Execute addPost Action / Mutation 10min
- 54. Update and Optimistic Response for addPost Mutation 11min
- 55. Infinite Scroll on Posts Page; Add typeDef, Resolver, and Query 8min
- 56. Add Infinite Scroll Functionality on Client 10min
- 57. Add Grid Layout / Cards for Each Post in Posts Component 10min
- Section 11: Post Component 0 / 7|44min
- 58. Create Post Component and Route 5min
- 59. Create and Execute getPost Query 7min
- 60. Build out Post Card in Post Component 8min
- 61. Add Messages ## Section to Post Component 6min
- 62. Create addPostMessage Mutation 6min
- 63. Perform addPostMessage in Post Component 8min
- 64. Add Validation for Message Input, Clear on Submit 5min
- Section 12: Like / Unlike Post 0 / 4|27min
- Section 13: Search Posts 0 / 4|22min
- Section 14: Profile Page, Update / Delete Posts 0 / 7|53min
- 73. Add User Details Card / Favorites Cards 6min
- 74. Write getUserPosts Query 4min
- 75. Execute getUserPosts Query, Create and Populate User Cards 9min
- 76. Add Edit Post Dialog for Updating User Posts 6min
- 77. Create updateUserPost Mutation 8min
- 78. Executing updateUserPost Mutation with Vuex Action 13min
- 79. deleteUserRecipe Mutation - Backend Creation to Frontend Execution 7min
- Section 15: Preparing for Deployment 0 / 3|17min
- Section 16: Deployment with Heroku / Now v2 0 / 1|13min
- Section 17: BONUS 0 / 1|1min
Related projects
| Project | Dates | Source Code |
|---|---|---|
| NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' course | 19/8/2019 | full-stack-vue-with-graphql-the-ultimate-guide-nuxt |
What I've learned
- Learn in-depth how to use Apollo Server 2 and Apollo Boost to create powerful full-stack apps
- Learn how to handle errors on the client and server with Apollo / GraphQL
- Be able to implement session-based JWT authentication to GraphQL applications
- Integrate Apollo with Vuex for more reliable and scalable state management
- Implement infinite scrolling functionality using Vue-Apollo
- Deploy full-stack JavaScript / GraphQL applications using Heroku and Netlify
- Learn how to write queries and mutations in the GraphQL language on both the client and server
- Make use of many useful MongoDB methods and features
- Be able to create attractive, sophisticated UIs using the Vuetify CSS framework
- Become more familiar with all the best ES6 / 7 features such as async / await, destructuring, spread operators, arrow functions, etc
Section 1: Introduction 0 / 3|29min
1. Preview our Completed App 18min













2. Tools Used/Required 4min
- We are going to use NodeJs.

- We are also going to use Visual Studio Code

- We'll be using MongoDB Atlas

- We'll also be using Now — Global Serverless Deployments

- We'll be using Chrome Canary

- We need to install
Vue.js devtools


3. Formatting Vue Templates (And More) with VS Code 6min
We need to install the following Visual Studio Extensions.

- Change
Vetur › Format › Default Formatter: HTMLtojs-beautify-html

- Ensure
editor: format on saveistrue

- We are going to install Prettier - Code formatter

- We are also going to install Graphql For VSCode

Section 2: What is GraphQL / Apollo? (Optional) 0 / 2|23min
4. What is GraphQL? Using the SWAPI GraphQL API 20min
- We are going to use the [SWAPI The Star Wars AP][https://www.swapi.co/] web site

request
https://swapi.co/api/people/1/`
response
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": ["https://swapi.co/api/species/1/"],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
}
We cannot filter by specific values or show only the data that we need.
We can use GraphiQL to make
GraphQLrequests.

- We only want the
name,heightandmass. WegraphQLis very easy.
request
{
person(personID: 1) {
name
height
mass
}
}
response
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"mass": 77
}
}
}
- We also can have all the information about all the queries available.




5. What is Apollo? 3min
- Apollo, is
the Data Graph Plataform, it helpsdo GraphQl Right. It's a single versatile query system to replace a patchwork of legacy APIs, with all the devtools and cloud services you need to run GraphQL at scale.

We are going to work with Apollo Server that is the best way to quickly build a production-ready, self-documenting API for GraphQL clients, using data from any source.
We are also going to work with Apollo Boost that is a great way to get started with Apollo Client quickly, but there are some advanced features it doesn't support out of the box, so with Apollo Boost migration we need to set Apollo Client up manually.
We are also going to work with Apollo and GraphQL for Vue.js that helps integrate GraphQL in your Vue.js apps!
Section 3: Intro to Apollo Server 2, Queries, Mutations and GraphQL Playground 0 / 5|27min
6. Git Clone and Install Dependencies (Required) 2min
We are going to need to clone the https://github.com/reedbarger/fullstack-vue-graphql-starter repository to start developing the server side of our application.
Create the
full-stack-vue-with-graphql-the-ultimate-guidefolder and copy the 2 files from the repository there.

- Install the dependecies by executing
npm install
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide
$ npm install
> bcrypt@3.0.6 install C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\bcrypt
> node-pre-gyp install --fallback-to-build
node-pre-gyp WARN Using needle for node-pre-gyp https download
[bcrypt] Success: "C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\bcrypt\lib\binding\bcrypt_lib.node" is installed via remote
> core-js@3.1.3 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> protobufjs@6.8.8 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\protobufjs
> node scripts/postinstall
> nodemon@1.19.1 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\nodemon
> node bin/postinstall || exit 0
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN apollo-graphql@0.3.2 requires a peer of graphql@^14.2.1 but none is installed. You must install peer dependencies yourself.
npm WARN fullstack-vue-graphql-starter@1.0.0 No repository field.
npm WARN fullstack-vue-graphql-starter@1.0.0 scripts['server'] should probably be scripts['start'].
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
added 477 packages from 357 contributors and audited 3188 packages in 39.25s
found 0 vulnerabilities
7. Initializing Apollo Server 2 (Optional) 7min
- We are going to create the
server.jsdocument to set up theApollo Server
server.js
const { ApolloServer, gql } = require("apollo-server");
const server = new ApolloServer({});
server.listen();
- We are going to execute the server
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm run server
> fullstack-vue-graphql-starter@1.0.0 server C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> nodemon server.js
[nodemon] 1.19.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node server.js`
C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-core\dist\ApolloServer.js:139
throw Error('Apollo Server requires either an existing schema, modules or typeDefs');
^
Error: Apollo Server requires either an existing schema, modules or typeDefs
at new ApolloServerBase (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-core\dist\ApolloServer.js:139:23)
at new ApolloServer (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-express\dist\ApolloServer.js:46:9)
at new ApolloServer (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server\dist\index.js:23:9)
at Object.<anonymous> (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\server.js:3:16)
at Module._compile (internal/modules/cjs/loader.js:805:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:816:10)
at Module.load (internal/modules/cjs/loader.js:672:32)
at tryModuleLoad (internal/modules/cjs/loader.js:612:12)
at Function.Module._load (internal/modules/cjs/loader.js:604:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:868:12)
[nodemon] app crashed - waiting for file changes before starting...
- We need to modify the
server.jsto add the minimum information needed.
server.js
const { ApolloServer, gql } = require("apollo-server");
const todos = [
{ task: "Wash car", completed: false },
{ task: "Clean room", completed: true }
];
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const server = new ApolloServer({ typeDefs });
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
[nodemon] app crashed - waiting for file changes before starting...
[nodemon] restarting due to changes...
[nodemon] starting `node server.js`
Server listening on http://localhost:4000/
8. Adding Resolvers and Executing Queries in GraphQL Playground (Optional) 6min
- if we access http://localhost:4000/ we can see the
GraphQL Playgroundis open.

- We need to modify the
server.jsto create the resolvers.
server.js
const { ApolloServer, gql } = require("apollo-server");
const todos = [
{ task: "Wash car", completed: false },
{ task: "Clean room", completed: true }
];
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const resolvers = {
Query: {
getTodos: () => todos
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can see now the
schema:

- We also can see
docs


- We can now create a
query
request
query {
getTodos {
task
completed
}
}
response
{
"data": {
"getTodos": [
{
"task": "Wash car",
"completed": false
},
{
"task": "Clean room",
"completed": true
}
]
}
}

9. Writing and Running First Mutation in GraphQL Playground (Optional) 8min
- We are going to modify the
server.jsto add the mutations.
server.js
const { ApolloServer, gql } = require("apollo-server");
const todos = [
{ task: "Wash car", completed: false },
{ task: "Clean room", completed: true }
];
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const resolvers = {
Query: {
getTodos: () => todos
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can see now that we have the
mutationsas well.


- We are going to test if it works.
request
mutation {
addTodo(task: "Eat lunch", completed: true) {
task
completed
}
}
mutation
{
"data": {
"addTodo": {
"task": "Eat lunch",
"completed": true
}
}
}
- We cah chekck if the new task has been added by using the
getTodosQuery again.
request
query {
getTodos {
task
completed
}
}
response
{
"data": {
"getTodos": [
{
"task": "Wash car",
"completed": false
},
{
"task": "Clean room",
"completed": true
},
{
"task": "Eat lunch",
"completed": true
}
]
}
}
10. Exploring GraphQL Playground 4min
- We can get a
curlcommand of our query or mutation by clickingCopy Curlbutton. It will copy it to theclipboard
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-al
ive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query {\n getTodos {\n task\n completed\n }\n}\n"}' --compressed
{"data":{"getTodos":[{"task":"Wash car","completed":false},{"task":"Clean room","completed":true},{"task":"Eat lunch","completed":true}]}}
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
- We can manage
Query variablesby clickingQUERY VARIABLES

- We can manage the
Http Headersby clickingHTTTP HEADERS

- We can add multiple
QueriesandMutationsby clicking the+button

- We can export the
Schemaby clickingSCHEMAand theDOWNLOAD


instrospectionSchema.json
{
"_queryType": "Query",
"_mutationType": "Mutation",
"_subscriptionType": null,
"_directives": [
{
"name": "cacheControl",
"description": "",
"locations": ["FIELD_DEFINITION", "OBJECT", "INTERFACE"],
"args": [
{
"name": "maxAge",
"description": "",
"type": "Int"
},
{
"name": "scope",
"description": "",
"type": "CacheControlScope"
}
]
},
{
"name": "skip",
"description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"args": [
{
"name": "if",
"description": "Skipped when true.",
"type": "Boolean!"
}
]
},
{
"name": "include",
"description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"args": [
{
"name": "if",
"description": "Included when true.",
"type": "Boolean!"
}
]
},
{
"name": "deprecated",
"description": "Marks an element of a GraphQL schema as no longer supported.",
"locations": ["FIELD_DEFINITION", "ENUM_VALUE"],
"args": [
{
"name": "reason",
"description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).",
"type": "String",
"defaultValue": "No longer supported"
}
]
}
],
"astNode": null,
"_typeMap": {
"Query": "Query",
"Todo": "Todo",
"String": "String",
"Boolean": "Boolean",
"Mutation": "Mutation",
"__Schema": "__Schema",
"__Type": "__Type",
"__TypeKind": "__TypeKind",
"__Field": "__Field",
"__InputValue": "__InputValue",
"__EnumValue": "__EnumValue",
"__Directive": "__Directive",
"__DirectiveLocation": "__DirectiveLocation",
"CacheControlScope": "CacheControlScope",
"Upload": "Upload",
"Int": "Int"
},
"_implementations": {}
}


schema.graphql
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE
enum CacheControlScope {
PUBLIC
PRIVATE
}
type Mutation {
addTodo(task: String, completed: Boolean): Todo
}
type Query {
getTodos: [Todo]
}
type Todo {
task: String
completed: Boolean
}
# The `Upload` scalar type
represents a file upload.
scalar Upload
- We can modifiy the setting by clicking on the
gearicon.

settings
{
"editor.cursorShape": "line",
"editor.fontFamily": "'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace",
"editor.fontSize": 14,
"editor.reuseHeaders": true,
"editor.theme": "dark",
"general.betaUpdates": false,
"prettier.printWidth": 80,
"prettier.tabWidth": 2,
"prettier.useTabs": false,
"request.credentials": "omit",
"schema.disableComments": true,
"schema.polling.enable": true,
"schema.polling.endpointFilter": "*localhost*",
"schema.polling.interval": 2000,
"tracing.hideTracingResponse": true,
"queryPlan.hideQueryPlanResponse": true
}
- We can see the lastest
Queriesandmutationsby clickingHISTORY


Section 4: Connect to MLab Database, Create Mongoose Models and GraphQL TypeDefs 0 / 7|54min
11. Create MongoDB Atlas Database, Connect to GraphQL Server 7min
- We are going to connect
MongoDB Altas


- Connect to
cluster0

Modify the
server.jsto removeClick on
Connect your Application

- Copy the connection String

- We need to create a new
.envdocument where we are going to put theenvironment variables
.env
MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
Note:- this document must be added to the .gitignore file.We need to modify the
server.jsto connectMongo Atlas
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
mongoose
.connect(process.env.MONGO_URI, { useNewUrlParser: true })
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const server = new ApolloServer({
typeDefs
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can try now to run the app
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm start dev
> fullstack-vue-graphql-starter@1.0.0 start C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> node server.js "dev"
Server listening on http://localhost:4000/
DB connected
12. Update! Connecting to MongoDB Atlas instead of MLab 4min
13. Creating Mongoose Schemas 11min
- We are going to create the models for the
MongoDb, starting by creating the newmodelsfolder and thePost.jsandUser.jsmodel documents.
model\User.js
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true,
trim: true
},
avatar: {
type: String
},
joinDate: {
type: Date,
default: Date.now
},
favorites: {
type: [mongoose.Schema.Types.ObjectId],
required: true,
ref: "Post"
}
});
module.exports = mongoose.model("User", UserSchema);
model\Post.js
const mongoose = require("mongoose");
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
imageUrl: {
type: String,
required: true
},
categories: {
type: [String],
required: true
},
description: {
type: String,
required: true
},
createdDate: {
type: Date,
default: Date.now
},
likes: {
type: Number,
default: 0
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User"
},
messages: [
{
messageBody: {
type: String,
required: true
},
messageDate: {
type: Date,
default: Date.now
},
messageUser: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User"
}
}
]
});
module.exports = mongoose.model("Post", PostSchema);
- We are going to modify the
server.jsto import themodels.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
const User = require("./models/User");
const Post = require("./models/Post");
mongoose
.connect(process.env.MONGO_URI, { useNewUrlParser: true })
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const server = new ApolloServer({
typeDefs,
context: {
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
14. Creating typeDefs for Project 8min
- We are going to create the
typeDefs.gqldocument that will contain the all thetypeDefsincluded in the solution.
typeDefs.glp
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
- We are going to modify the
server.jsto import thetypeDefs.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const User = require("./models/User");
const Post = require("./models/Post");
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const server = new ApolloServer({
typeDefs,
context: {
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
15. Write and Run signupUser Mutation 9min
- We are going to modify the
typeDefs.gqldocument to include thegetUserquery and thesignupUsermutation.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Query {
getUser: User
}
type Mutation {
signupUser(username: String!, email: String!, password: String!): User!
}
- We are going to create the
resolvers.jsdocument that is going to be used to resolve thequeryand themutation.
. resolvers.js
module.exports = {
Query: {
getUser: () => null
},
Mutation: {
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return newUser;
}
}
};
- We are going to modify the
server.jsdocument to import theresolversdocument.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const server = new ApolloServer({
typeDefs,
resolvers,
context: {
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can test now if the new mutation is working.
request
mutation {
signupUser(username: "John", email: "john@gmail.com", password: "Password") {
_id
username
email
avatar
password
joinDate
}
}
response
{
"data": {
"signupUser": {
"_id": "5d04cf30deb8673e8c38fc2d",
"username": "John",
"email": "john@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "Sat Jun 15 2019 11:57:52 GMT+0100 (Irish Standard Time)"
}
}
}


16. Write and Run addPost Mutation 7min
- We are going to modify the
typeDefs.gqldocument to add theaddPostmutation.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Query {
getUser: User
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signupUser(username: String!, email: String!, password: String!): User!
}
- We are also going to modify the
resolvers.jsdocument to add the resolver for theaddPostmutation.
resolvers.js
module.exports = {
Query: {
getUser: () => null
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return newUser;
}
}
};
- We are going to try to create a new
Postusing theaddPostmutation.
request
mutation {
addPost(
title: "Mona lisa"
imageUrl: "google.com"
categories: ["Art"]
description: "A painting"
creatorId: "5d04cf30deb8673e8c38fc2d"
) {
title
imageUrl
categories
description
createdDate
likes
}
}
response
{
"data": {
"addPost": {
"title": "Mona lisa",
"imageUrl": "google.com",
"categories": ["Art"],
"description": "A painting",
"createdDate": "Sat Jun 15 2019 12:16:07 GMT+0100 (Irish Standard Time)",
"likes": 0
}
}
}

17. Write and Run getPosts Query, Intro to populate 7min
- We are going to modify the
typeDefs.gqldocument to add thegetPostsquery.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Query {
getPosts: [Post]
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signupUser(username: String!, email: String!, password: String!): User!
}
- We are also going to modify the
resolvers.jsdocument to add the resolver for thegetPostsquery.
resolvers.js
module.exports = {
Query: {
getPosts: async (_, args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return newUser;
}
}
};
- We are going to try to check if the new
getPostsquery works properly.
request
query {
getPosts {
title
imageUrl
categories
description
createdDate
likes
createdBy {
_id
username
email
avatar
password
joinDate
}
}
}
response
{
"data": {
"getPosts": [
{
"title": "Mona lisa",
"imageUrl": "https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.readingpublicmuseum.org%2Fexhibit_secrets-of-mona-lisa-4.jpg&f=1",
"categories": ["Art"],
"description": "A painting",
"createdDate": "Sat Jun 15 2019 12:16:07 GMT+0100 (Irish Standard Time)",
"likes": 0,
"createdBy": {
"_id": "5d04cf30deb8673e8c38fc2d",
"username": "John",
"email": "john@gmail.com",
"avatar": null,
"password": "Password",
"joinDate": "Sat Jun 15 2019 11:57:52 GMT+0100 (Irish Standard Time)"
}
}
]
}
}
Section 5: Create Vue Frontend with Vue CLI 3 0 / 8|57min
18. Create Vue Client with Vue-CLI 3 8min
- We are goinf to use Vue CLI 3 to create our Vuw Client

- We need to ensure we have
Node.jsversion 8.9 or above (8.11.0+ recommended).
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ node --versionv11.13.0
- We can install the tool globally by executing
npm install -g @vue/cli:
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm install -g @vue/cli
C:\Users\juan.pablo.perez\AppData\Roaming\npm\vue -> C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@vue\cli\bin\vue.js
> core-js@3.1.4 postinstall C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\core-js
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> core-js-pure@3.1.4 postinstall C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\core-js-pure
> node scripts/postinstall || echo "ignore"
Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
The project needs your help! Please consider supporting of core-js on Open Collective or Patreon:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
> protobufjs@6.8.8 postinstall C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\protobufjs
> node scripts/postinstall
> nodemon@1.19.1 postinstall C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\nodemon
> node bin/postinstall || exit 0
Love nodemon? You can now support the project via the open collective:
> https://opencollective.com/nodemon/donate
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\@vue\cli\node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @vue/cli@3.8.4
added 807 packages from 541 contributors in 222.37s
- We can run the
Vue CLI User Interfaceby executingvue ui
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ vue ui
� Starting GUI...
� Ready on http://localhost:8001


- Put
clientforProject folder,npmforPackage manager, unselectInitilize git repository (recommender)forGit repositoryand click onNext

- Select
ManualforSelect a presetand click onNext

- Select
Router,Vuex, unselectLinter / Formatterand click onNext

- Select
Use history moder for Routerand click onCreate Project

- Click on
Continue without saving

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ vue ui
� Starting GUI...
� Ready on http://localhost:8001
✨ Creating project in C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client.
⚓ Running completion hooks...
� Generating README.md...

INFO
Vue CLI v3.8.4
15/06/2019, 16:02:34
🌠 Ready on http://localhost:8001
15/06/2019, 16:02:36
INFO
⚙ Installing CLI plugins. This might take a while...
15/06/2019, 16:41:27
INFO
> yorkie@2.0.0 install C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client\node_modules\yorkie
> node bin/install.js
15/06/2019, 16:44:56
INFO
setting up Git hooks
15/06/2019, 16:44:56
INFO
can't find .git directory, skipping Git hooks installation
15/06/2019, 16:44:56
INFO
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
15/06/2019, 16:44:57
INFO
added 1078 packages from 891 contributors and audited 18077 packages in 206.891s
15/06/2019, 16:45:02
INFO
found 0 vulnerabilities
15/06/2019, 16:45:02
INFO
🚀 Invoking generators...
15/06/2019, 16:45:02
INFO
📦 Installing additional dependencies...
15/06/2019, 16:45:04
INFO
added 4 packages from 1 contributor and audited 18083 packages in 30.472s
15/06/2019, 16:45:38
INFO
found 0 vulnerabilities
15/06/2019, 16:45:38
INFO
🎉 Successfully created project client.
15/06/2019, 16:45:39

19. Adding Plugins with Vue GUI and Concurrently Dev Script 4min
- If we click on
pluginswe can see the plugins already installed.

- If we click on
+Add pluginwe can see a list of availableplugins


- We missed adding Vuex, so we can do it by clicking on
Add Vuexbutton.



Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (master)
$ vue ui
� Starting GUI...
� Ready on http://localhost:8001
� Invoking generator for core:vuex...
� Installing additional dependencies...
added 1 package from 1 contributor and audited 18084 packages in 26.979s
found 0 vulnerabilities
✔ Successfully invoked generator for plugin: core:vuex
The following files have been updated / added:
.gitignore
README.md
babel.config.js
package-lock.json
package.json
public/favicon.ico
public/index.html
src/App.vue
src/assets/logo.png
src/components/HelloWorld.vue
src/main.js
src/router.js
src/store.js
src/views/About.vue
src/views/Home.vue
You should review these changes with git diff and commit them.
- We need to modify the
package.jsonto theclientscript fromcd client && npm starttocd client && npm run servebecause it is how the script is called on theclient package.jsondocument.
package.json
{
"name": "fullstack-vue-graphql-starter",
"version": "1.0.0",
"description": "Starter for Full-Stack Vue-GraphQL-Apollo Projects",
"main": "server.js",
"scripts": {
"server": "nodemon server.js",
"client": "cd client && npm serve",
"dev": "concurrently --names \"server,client\" \"npm run server --silent\" \"npm run client --silent\""
},
"keywords": [],
"author": "Reed Barger",
"license": "ISC",
"dependencies": {
"apollo-server": "^2.0.0-rc.7",
"bcrypt": "^3.0.0",
"dotenv": "^6.0.0",
"graphql": "^0.13.2",
"jsonwebtoken": "^8.3.0",
"md5": "^2.2.1",
"mongoose": "^5.2.6"
},
"devDependencies": {
"concurrently": "^3.6.0",
"nodemon": "^1.18.1"
}
}
client\package.json
{
"name": "client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"core-js": "^2.6.5",
"vue": "^2.6.10",
"vue-router": "^3.0.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"vue-template-compiler": "^2.6.10"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
- If we execute
npm run dev, bothserverandclientare going to be executed together.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm run dev
> fullstack-vue-graphql-starter@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> concurrently --names "server,client" "npm run server --silent" "npm run client --silent"
[server] [nodemon] 1.19.1
[server] [nodemon] to restart at any time, enter `rs`
[server] [nodemon] watching: *.*
[server] [nodemon] starting `node server.js`
[client] INFO Starting development server...
[server] Server listening on http://localhost:4000/
[server] DB connected
[client] 98% after emitting CopyPlugin DONE Compiled successfully in 7344ms5:17:19 PM
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.1.49:8080/
[client]
[client] Note that the development build is not optimized.
[client] To create a production build, run npm run build.
[client]

20. Structuring our Vue App 4min
- We are going to modify the
client\src\App.vue
client\src\App.vue
<template>
<div>
<h1>App</h1>
<router-view />
</div>
</template>
- We are going to move the
client\src\views\Home.vuetoclient\src\components\Home.vueand then remove theclient\src\viewsfolder. We are also going to remove thesrc\assetsfolder and thesrc\components\HelloWorld.vuedocument.

- We are going to modify the
client\src\components\Home.vuedocument
client\src\components\Home.vue
<template>
<div>
<h1>Home</h1>
</div>
</template>
<script>
export default {
name: "home"
};
</script>
- The
client\src\main.jsdocument is fine.
client\src\main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- We are going to modify the
client\src\router.jsdocument.
client\src\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
}
]
});
- The
client\src\store.jsdocument is fine.
client\src\store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
}
})
- We can check if everything works correctly.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm run dev
> fullstack-vue-graphql-starter@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> concurrently --names "server,client" "npm run server --silent" "npm run client --silent"
[server] [nodemon] 1.19.1
[server] [nodemon] to restart at any time, enter `rs`
[server] [nodemon] watching: *.*
[server] [nodemon] starting `node server.js`
[client] INFO Starting development server...
[server] Server listening on http://localhost:4000/
[server] DB connected
[client] 98% after emitting CopyPlugin DONE Compiled successfully in 4890ms5:35:51 PM
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.1.49:8080/
[client]
[client] Note that the development build is not optimized.
[client] To create a production build, run npm run build.

21. Installing Vuetify Plugin and Generating a Theme 10min
- We are going to use the Vuetify Material Design Component Framework.

- We can go to Vue UI to see how we can install
Vuetifyby usingVue CLI-3

- We are going to install it by using the
vue add vuetifycommand.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ cd client
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (master)
$ vue add vuetify
� Installing vue-cli-plugin-vuetify...
+ vue-cli-plugin-vuetify@0.5.0
added 1 package from 1 contributor and audited 18085 packages in 25.338s
found 0 vulnerabilities
✔ Successfully installed plugin: vue-cli-plugin-vuetify
? Choose a preset: Default (recommended)
� Invoking generator for vue-cli-plugin-vuetify...
� Installing additional dependencies...
added 11 packages from 49 contributors and audited 18123 packages in 47.879s
found 0 vulnerabilities
⚓ Running completion hooks...
✔ Successfully invoked generator for plugin: vue-cli-plugin-vuetify
The following files have been updated / added:
.gitignore
README.md
babel.config.js
package-lock.json
package.json
public/favicon.ico
public/index.html
src/App.vue
src/assets/logo.svg
src/components/HelloWorld.vue
src/components/Home.vue
src/main.js
src/plugins/vuetify.js
src/router.js
src/store.js
src/views/Home.vue
You should review these changes with git diff and commit them.
- We can see the
client\src\plugins\vuetify.jshas been added.
client\src\plugins\vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'
Vue.use(Vuetify, {
iconfont: 'md',
})
- The
client\public\index.htmlhas been modified:
<!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">
<title>client</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
</head>
<body>
<noscript>
<strong>We're sorry but client 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>
- We can use the VUETIFYTHEME GENERATOR tool to generate a
themefor our application.






- We are going to modify the
client\src\plugins\vuetify.jsdocument to add thethemecolours`.
client\src\plugins\vuetify.js
import Vue from 'vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/src/stylus/app.styl'
Vue.use(Vuetify, {
iconfont: 'md',
theme: {
primary: "#3B125F",
secondary: "#8B5FBF",
accent: "#BF653F",
error: "#722530",
warning: "#A37513",
info: "#396893",
success: "#4caf50"
}
})
- We are going to modify the
client\src\components\Home.vuedocument to add a button to see if the newthemeworks.
client\src\components\Home.vue
<template>
<div>
<h1>Home</h1>
<v-btn color="primary">Button</v-btn>
</div>
</template>
<script>
export default {
name: "home"
};
</script>
- We are going to modify the
client\src\App.vuedocument to put the mandatory<v-app>html tag.
client\src\components\Home.vue
<template>
<v-app>
<h1>App</h1>
<router-view />
</v-app>
</template>
- We can test if it works.

22. Coolors.co for Creating Great Color Schemes (Optional) 4min
- We can also use Coolors,
The super fast color schemes generator!




palette.scss
/* HSL */
$color1: hsla(215%, 64%, 86%, 1);
$color2: hsla(129%, 15%, 45%, 1);
$color3: hsla(30%, 38%, 82%, 1);
$color4: hsla(161%, 40%, 70%, 1);
$color5: hsla(153%, 57%, 70%, 1);
/* RGB */
$color1: rgba(196, 215, 242, 1);
$color2: rgba(97, 130, 102, 1);
$color3: rgba(226, 208, 190, 1);
$color4: rgba(148, 209, 190, 1);
$color5: rgba(134, 222, 183, 1);
23. Horizontal Navbar and Mobile First Design 12min
- We are going to modify the
client\src\App.vuedocument to create the layout for our app usingVuetify
client\src\App.vue
<template>
<v-app>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<router-view />
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
}
};
</script>
- We are also going to modify the
client\src\components\Home.vuedocument to put the component inside a<v-container>Vuetify tag.
client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
</v-container>
</template>
<script>
export default {
name: "home"
};
</script>
- We are going to test if it works.



24. Add Side Navbar 7min
- We are going to modify the
client\src\App.vuedocument to add aSide Navbarp usingVuetify
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<router-view/>
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
},
sideNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
- We are going to check if it works properly.



25. Add Routing and Page Transitions 9min
- We are going to create the
client\src\components\Authfolder with theProfile.vue,Signin.vueandSignup.vuedocuments inside.
client\src\components\Auth\Profile.vue
<template>
<v-container>
<h1>Profile</h1>
</v-container>
</template>
<script>
export default {
name: "Profile"
};
</script>
client\src\components\Auth\Signin.vue
<template>
<v-container>
<h1>Signin</h1>
</v-container>
</template>
<script>
export default {
name: "Signin"
};
</script>
client\src\components\Auth\Signup.vue
<template>
<v-container>
<h1>Signup</h1>
</v-container>
</template>
<script>
export default {
name: "Signup"
};
</script>
- We are also going to create the
client\src\components\Postsfolder with theAddPost.vueandPosts.vuedocuments inside.
client\src\components\Posts\AddPost.vue
<template>
<v-container>
<h1>Add Post</h1>
</v-container>
</template>
<script>
export default {
name: "AddPost"
};
</script>
client\src\components\Posts\Posts.vue
<template>
<v-container>
<h1>Posts</h1>
</v-container>
</template>
<script>
export default {
name: "Posts"
};
</script>
- We need to modify the
client\router.jsto include the newcomponents
client\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
import AddPost from './components/Posts/AddPost.vue'
import Posts from './components/Posts/Posts.vue'
import Profile from './components/Auth/Profile.vue'
import Signin from './components/Auth/Signin.vue'
import Signup from './components/Auth/Signup.vue'
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/posts",
name: "Posts",
component: Posts
},
{
path: "/post/add",
name: "AddPost",
component: AddPost
},
{
path: "/profile",
name: "Profile",
component: Profile
},
{
path: "/signin",
name: "Signin",
component: Signin
},
{
path: "/Signup",
name: "Signup",
component: Signup
}
]
});
- We are going to modify the
client\src\App.vuedocument to addPage Transitions
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view/>
</transition>
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
},
sideNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
- We can now test if it works.





Section 6: Using Vue Apollo 0 / 4|24min
26. Setting up Apollo Client / Vue Apollo, Firing getPosts Query from Client 8min
- We need to install the
apollo-boostand thevue-apollopackages.
juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (master)
$ npm i apollo-boost vue-apollo
npm WARN apollo-boost@0.4.3 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link@1.2.12 requires a peer of graphql@^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN graphql-tag@2.10.1 requires a peer of graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link-http@1.5.15 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-cache-inmemory@1.6.2 requires a peer of graphql@0.11.7 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-client@2.6.3 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-utilities@1.3.2 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link-http-common@0.2.14 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ apollo-boost@0.4.3
+ vue-apollo@3.0.0-rc.1
added 20 packages from 22 contributors and audited 18290 packages in 46.13s
found 0 vulnerabilities
- We need tp modify the
client/main.jsdocument to set up the use of Apollo.
client/main.js
import "@babel/polyfill";
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";
Vue.use(VueApollo);
// Setup ApolloClient
const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql"
});
const apolloProvider = new VueApollo({ defaultClient });
Vue.config.productionTip = false;
new Vue({
apolloProvider,
router,
store,
render: h => h(App)
}).$mount("#app");
- We are going to modify the
client/components/Home.vuedocument to include all theposts
client/components/Home.vue
<template>
<v-container>
<h1>Home</h1>
<ul v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
}
}
};
</script>
- We need to modify the
typeDefs.gpldocument to include theIdfield for thePosttype.
typeDefs.gpl
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Query {
getPosts: [Post]
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signupUser(username: String!, email: String!, password: String!): User!
}
- We need to check if everything works properly


27. Dive into Smart Queries in Vue Components 7min
- We can change the
\client\src\components\Home.vuedocument to make aloading...test show whenApollois getting the information from the Server.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<div v-if="$apollo.loading">loading...</div>
<ul v-else v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
}
}
};
</script>

- We can use the
resultmethod ofApolloto get information from the response and store it in aVue.JSproperty. There are some other methods likenetworkStatus.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<div v-if="$apollo.loading">loading...</div>
<ul v-else v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
posts: []
}
},
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`,
result({ data, loading, networkStatus }) {
if (!loading) {
this.posts = data.getPosts;
console.log('[networkStatus]', networkStatus)
}
}
}
}
};
</script>


- We can see all the arguments of the
resultmethod by puting their object to the console.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<div v-if="$apollo.loading">loading...</div>
<ul v-else v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
posts: []
}
},
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`,
result(args) {
console.log(args)
}
}
}
};
</script>

- We have another method called
errorwhere we can obtain all the errors that can happen when executing theGraphQLquery.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<div v-if="$apollo.loading">loading...</div>
<ul v-else v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
posts: []
}
},
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`,
result(args) {
console.log(args)
},
error(err) {
console.log('[ERROR!!]', err)
console.dir(err)
}
}
}
};
</script>

- We can get more information in Apollo and GraphQL for Vue.js



28. Executing Queries with the ApolloQuery Component 6min
- We have another way of executing Queries and it is by using the
ApolloQuery` Component.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, data} }">
<div v-if="loading">loading...</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>

- We can even manage the errors.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, error, data } }">
<div v-if="loading">loading...</div>
<div v-else-if="error">Error! {{error.message}}</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPost {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>

- We can also obtain the
Network Status
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, error, data, networkStatus } }">
<div v-if="loading">loading...</div>
<div v-else-if="error">Error! {{error.message}}</div>
<div v-else-if="networkStatus">Network Status: {{networkStatus}}</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>

29. Add Carousel Component to Home Page 3min
- We are going to use the
Corousel Vuetifycomponents.
\client\src\components\Home.vue
<template>
<v-container text-xs-center v-if="getPosts">
<v-flex xs12>
<v-carousel v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in getPosts" :key="post._id" :src="post.imageUrl">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>


Section 7: Integrate Vuex with ApolloClient 0 / 4|23min
30. Firing getPosts Action with Vuex 7min
- We are going to modify the
Home.Vuecomponent, themain.jsmain document and thestore.jsstore document to use the Vuex store to call theGraphQL getPostsquery.
client\src\main.js
import "@babel/polyfill";
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";
Vue.use(VueApollo);
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql"
});
const apolloProvider = new VueApollo({ defaultClient });
Vue.config.productionTip = false;
new Vue({
apolloProvider,
router,
store,
render: h => h(App)
}).$mount("#app");
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import { gql } from "apollo-boost";
import { defaultClient as apolloClient } from "./main";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {
getPosts: () => {
// use ApolloClient to fire getPosts query
apolloClient
.query({
query: gql`
query {
getPosts {
_id
title
imageUrl
}
}
`
})
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
}
}
});
client\src\components\Home.vue
<template>
<v-container text-xs-center v-if="getPosts">
<!-- <v-flex xs12>
<v-carousel v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in getPosts" :key="post._id" :src="post.imageUrl">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex> -->
</v-container>
</template>
<script>
// import { gql } from "apollo-boost";
export default {
name: "home",
created() {
this.handleGetCarouselPosts();
},
methods: {
handleGetCarouselPosts() {
// reach out to Vuex store, fire action that gets posts for carousel
this.$store.dispatch("getPosts");
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>

31. Using Mutations and Getters 8min
- We are going to modify the
Home.Vuecomponent and thestore.jsstore document again to set up properly the reading of the posts using the store.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import { gql } from "apollo-boost";
import { defaultClient as apolloClient } from "./main";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: []
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
}
},
actions: {
getPosts: ({ commit }) => {
// use ApolloClient to fire getPosts query
apolloClient
.query({
query: gql`
query {
getPosts {
_id
title
imageUrl
}
}
`
})
.then(({ data }) => {
// Get data from actions to state via mutations
// commit passes data from actions along to mutation functions
commit("setPosts", data.getPosts);
console.log(data.getPosts);
})
.catch(err => {
console.error(err);
});
}
},
getters: {
posts: state => state.posts
}
});
client\src\components\Home.vue
<template>
<v-container text-xs-center>
<v-flex xs12>
<v-carousel v-if="posts.length > 0" v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in posts" :key="post._id" :src="post.imageUrl">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
created() {
this.handleGetCarouselPosts();
},
computed: {
posts() {
return this.$store.getters.posts;
}
},
methods: {
handleGetCarouselPosts() {
// reach out to Vuex store, fire action that gets posts for carousel
this.$store.dispatch("getPosts");
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>

32. Add Loading Property, Loading Spinner and mapGetters 6min
- We are going to modify the
Home.Vuecomponent and thestore.jsstore document again to add a Loading property in the store, a loading Spinner and to use mapGetters.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import { gql } from "apollo-boost";
import { defaultClient as apolloClient } from "./main";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
}
},
actions: {
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: gql`
query {
getPosts {
_id
title
imageUrl
}
}
`
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
}
},
getters: {
posts: state => state.posts,
loading: state => state.loading
}
});
client\src\components\Home.vue
<template>
<v-container text-xs-center>
<v-layout row>
<v-dialog v-model="loading" persistent fullscreen>
<v-container fill-height>
<v-layout row justify-center align-center>
<v-progress-circular indeterminate :size="70" :width="7" color="secondary"></v-progress-circular>
</v-layout>
</v-container>
</v-dialog>
</v-layout>
<v-flex xs12>
<v-carousel v-if="!loading && posts.length > 0" v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in posts" :key="post._id" :src="post.imageUrl">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "home",
created() {
this.handleGetCarouselPosts();
},
computed: {
...mapGetters(["loading", "posts"])
},
methods: {
handleGetCarouselPosts() {
// reach out to Vuex store, fire action that gets posts for carousel
this.$store.dispatch("getPosts");
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>


33. Create queries.js for Clientside Query / Mutation Definitions 3min
- We are going to create the
queries.jsto put all GraphQL queries and mutations that we are going to use.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
/* User Queries */
/* Posts Mutations */
/* User Mutations */
- We are going to modify the
store.jsstore document again to use the newqueries.jsdocument.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import { defaultClient as apolloClient } from "./main";
import { GET_POSTS } from "./queries";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
}
},
actions: {
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
}
},
getters: {
posts: state => state.posts,
loading: state => state.loading
}
});

Section 8: JWT Authentication for Signin / Signup 0 / 12|1hr 27min
34. Create Gravatar Avatar and Hash User Passwords on Signup 7min
- We are going to modify the
User.jsdocument to assign a ramndom avatar to the user and to store the password hashed.
models\User.js
const mongoose = require("mongoose");
const md5 = require("md5");
const bcrypt = require("bcrypt");
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
password: {
type: String,
required: true,
trim: true
},
avatar: {
type: String
},
joinDate: {
type: Date,
default: Date.now
},
favorites: {
type: [mongoose.Schema.Types.ObjectId],
required: true,
ref: "Post"
}
});
// Create and add avatar to user
UserSchema.pre("save", function(next) {
this.avatar = `http://gravatar.com/avatar/${md5(this.username)}?d=identicon`;
next();
});
// Hash password so it can't be seen w/ access to database
UserSchema.pre("save", function(next) {
if (!this.isModified("password")) {
return next();
}
bcrypt.genSalt(10, (err, salt) => {
if (err) return next(err);
bcrypt.hash(this.password, salt, (err, hash) => {
if (err) return next(err);
this.password = hash;
next();
});
});
});
module.exports = mongoose.model("User", UserSchema);
- We can test if it works
request
mutation {
signupUser(username: "Fofo", email: "Fofo@gmail.com", password: "Fofo") {
_id
username
email
avatar
password
joinDate
}
}
response
{
"data": {
"signupUser": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"email": "Fofo@gmail.com",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon",
"password": "$2b$10$qb.j.FtbHSfSgyH8ZYGGwOW.cIFj8voPEs1m/jcmdFrX/j/kP1zky",
"joinDate": "Mon Jul 22 2019 05:18:23 GMT+0100 (Irish Standard Time)"
}
}
}
![]()
- We can see the avatar assigned by accessing the http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon url
![]()
35. Write and Run signinUser Mutation 5min
- We are going to create a new
GraphQL mutationto sign in a new user. Initially it's going to return the own user's data when the user and password are correct.
typeDefs.gql --> Add the new signinUser mutation.
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Query {
getPosts: [Post]
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signinUser(username: String!, password: String!): User
signupUser(username: String!, email: String!, password: String!): User!
}
resolvers.js --> Add the new signinUser mutation.
const bcrypt = require("bcrypt");
module.exports = {
Query: {
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return user;
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return newUser;
}
}
};
Request
mutation {
signinUser(username: "Fofo", password: "Fofo") {
_id
username
email
avatar
password
joinDate
}
}
Response
{
"data": {
"signinUser": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"email": "Fofo@gmail.com",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon",
"password": "$2b$10$qb.j.FtbHSfSgyH8ZYGGwOW.cIFj8voPEs1m/jcmdFrX/j/kP1zky",
"joinDate": "Mon Jul 22 2019 05:18:23 GMT+0100 (Irish Standard Time)"
}
}
}
- The password is case sensitive.
Request
mutation {
signinUser(username: "Fofo", password: "fofo") {
_id
username
email
avatar
password
joinDate
}
}
Response
{
"errors": [
{
"message": "Invalid password",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"signinUser"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Invalid password",
" at signinUser (C:\\Work\\Training\\Pre\\VueJs\\full-stack-vue-with-graphql-the-ultimate-guide\\resolvers.js:37:15)"
]
}
}
}
],
"data": {
"signinUser": null
}
}
36. Sign Token and Return it Upon Signin/Signup 8min
We are going to modify the
signinUserandsignupUsermutations to return a token with the information about the authenticated user instead of the information of the own user.We need to create the new
SECRETenvironment variable:
server\fake.env
MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
SECRET=mysupersecretencryptionkey
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type Query {
getPosts: [Post]
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getPosts: async (_, args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We can test if it works for the
signinUsermutation.
Request
mutation {
signinUser(username: "Fofo", password: "Fofo") {
token
}
}
Response
{
"data": {
"signinUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTYzOTg0NjIxLCJleHAiOjE1NjM5ODgyMjF9.8bqq_3lK-IyDbgxA5TnRkjobwU8o_x9XcEsEB8pz5FQ"
}
}
}
- We can test if it works for the
signupUsermutation.
Request
mutation {
signupUser(username: "Miliki", email: "miliki@gmail.com", password: "Miliki") {
token
}
}
Response
{
"data": {
"signupUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1pbGlraSIsImVtYWlsIjoibWlsaWtpQGdtYWlsLmNvbSIsImlhdCI6MTU2Mzk4NDc2MCwiZXhwIjoxNTYzOTg4MzYwfQ.0EoTKaXIBm3OIZDSPVVKE2iDLLaIJ4Vjtf1B3OltYgo"
}
}
}
37. Using Variables in GraphQL, Signin / Signup Mutation Defs 6min
With GraphQL is possible to use Variables with our Queries and Mutations
- We can test if it works for the
signupUsermutation.
Request
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
Variables:
{
"username": "Fofo",
"password": "Fofo"
}
Response
{
"data": {
"signinUser": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTYzOTg1NDY2LCJleHAiOjE1NjM5ODkwNjZ9.KD9J3c0vp-fxaS-fZR5CuZ1Wt3v_Lj3d1839IP720is"
}
}
}

- We are going to modify the
queries.jsdocument in theclientproject to add the newsigninandsignupmutations.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
/* User Queries */
/* Posts Mutations */
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
38. Add Signin Form, Write and Run signinUser Action, Return JWT 9min
- We are going to need to modify the store to add the
signinUseraction.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import { defaultClient as apolloClient } from "./main";
import { GET_POSTS, SIGNIN_USER } from "./queries";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
}
},
actions: {
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
signinUser: (_, payload) => {
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
console.log(data.signinUser);
})
.catch(err => {
console.error(err);
});
}
},
getters: {
posts: state => state.posts,
loading: state => state.loading
}
});
- We are also going to need to modify the
Signin.vuecomponent to make a properSigninform.
client\src\components\Auth\Signin.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signin Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>Welcome Back!</h1>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="secondary" dark>
<v-container>
<v-form @submit.prevent="handleSigninUser">
<v-layout row>
<v-flex xs12>
<v-text-field v-model="username" prepend-icon="face" label="Username" type="text" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field v-model="password" prepend-icon="extension" label="Password" type="password" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn color="accent" type="submit">Signin</v-btn>
<h3>Don't have an account?
<router-link to="/signup">Signup</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
name: "Signin",
data() {
return {
username: "",
password: ""
};
},
methods: {
handleSigninUser() {
this.$store.dispatch("signinUser", {
username: this.username,
password: this.password
});
}
}
};
</script>
- We are going to test if it works.


39. Additional Config for ApolloClient, Send Token from LocalStorage 8min
- We are going to need to modify the store to add the store the token in the
localStore.
client\src\store.js
.
.
.
signinUser: (_, payload) => {
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
localStorage.setItem("token", data.signinUser.token);
// console.log(data.signinUser);
})
.catch(err => {
console.error(err);
});
}
.
.
.
- We can check if the
localStoragecontains the token.

- We are going to modify in the client project the
main.jsdocument to manage how to send thetokenautomatically to our GraphQLqueriesandmutations.
\client\src\main.js
import "@babel/polyfill";
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";
Vue.use(VueApollo);
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql",
// include auth token with requests made to backend
fetchOptions: {
credentials: "include"
},
request: operation => {
// if no token with key of 'token' in localStorage, add it
if (!localStorage.token) {
localStorage.setItem("token", "");
}
// operation adds the token to an authorization header, which is sent to backend
operation.setContext({
headers: {
authorization: localStorage.getItem("token")
}
});
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError);
}
if (graphQLErrors) {
for (let err of graphQLErrors) {
console.dir(err);
}
}
}
});
const apolloProvider = new VueApollo({ defaultClient });
Vue.config.productionTip = false;
new Vue({
apolloProvider,
router,
store,
render: h => h(App)
}).$mount("#app");
We are going to check if it works properly.
If we send invalid credentials we can see the errors.

- If we send the proper credentials.

40. Verify JWT Token in server.js, Pass Result to currentUser in Context 7min
- We are going to verify if the Token is valid from the
Apollo servermethod in theserver.jsdocument.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Create Apollo GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
console.log(req.headers["authorization"]);
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- Now, if we try to authenticate again we should see the token on the
serverconsole.
[client] Server listening on http://localhost:4000/
[server] DB connected
[server] [nodemon] restarting due to changes...
[server] [nodemon] starting `node server.js`
[server] Server listening on http://localhost:4000/
[server] DB connected
[server] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkZvZm8iLCJlbWFpbCI6IkZvZm9AZ21haWwuY29tIiwiaWF0IjoxNTYzOTkwMDQwLCJleHAiOjE1NjM5OTM2NDB9.-0rHA0SpN-2otxZn92DyvD7l2ZpHTjDzdrlwMC49M
- We need now to validate it by modifying the
server.jsdocument again.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Verify JWT Token passed from client
const getUser = async token => {
if (token) {
try {
let user = await jwt.verify(token, process.env.SECRET);
console.log(user)
} catch (err) {
throw new AuthenticationError(
"Your session has ended. Please sign in again."
);
}
}
};
// Create Apollo/GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers["authorization"];
return { User, Post, currentUser: await getUser(token) };
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can check it again.
[server] [nodemon] restarting due to changes...
[server] [nodemon] starting `node server.js`
[server] Server listening on http://localhost:4000/
[server] DB connected
[server] { username: 'Fofo',
[server] email: 'Fofo@gmail.com',
[server] iat: 1563993293,
[server] exp: 1563996893 }
- The final code using the apollo
AuthenticationErrorclass will be like this.
server.js
const { ApolloServer, AuthenticationError } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Verify JWT Token passed from client
const getUser = async token => {
if (token) {
try {
return await jwt.verify(token, process.env.SECRET);
} catch (err) {
throw new AuthenticationError(
"Your session has ended. Please sign in again."
);
}
}
};
// Create Apollo/GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers["authorization"];
return { User, Post, currentUser: await getUser(token) };
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
41. Create getCurrentUser Query, Execute it from main.js 9min
- we are going to create the new
getCurrentUser Queryby modifying thetypeDefs.gqlandresolvers.jsdocuments.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type Query {
getCurrentUser: User
getPosts: [Post]
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We are going to modify the
Client store.jsdocument to create theuserstate, thesetUsermutation, thegetCurrentUseraction and theusergetter.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import router from "./router";
import { defaultClient as apolloClient } from "./main";
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER } from "./queries";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setUser: (state, payload) => {
state.user = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false);
// Add user data to state
commit("setUser", data.getCurrentUser);
console.log(data.getCurrentUser);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
signinUser: (_, payload) => {
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
localStorage.setItem("token", data.signinUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
console.error(err);
});
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading
}
});
- We are going to modify the
Client queries.jsdocument to include theGET_CURRENT_USERuserQUERY
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
/* Posts Mutations */
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We are going to modify the
Client main.jsdocument to force to execute thegetCurrentUseraction when the app is loaded.
client\src\main.js
import "@babel/polyfill";
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";
Vue.use(VueApollo);
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql",
// include auth token with requests made to backend
fetchOptions: {
credentials: "include"
},
request: operation => {
// if no token with key of 'token' in localStorage, add it
if (!localStorage.token) {
localStorage.setItem("token", "");
}
// operation adds the token to an authorization header, which is sent to backend
operation.setContext({
headers: {
authorization: localStorage.getItem("token")
}
});
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError);
}
if (graphQLErrors) {
for (let err of graphQLErrors) {
console.dir(err);
}
}
}
});
const apolloProvider = new VueApollo({ defaultClient });
Vue.config.productionTip = false;
new Vue({
apolloProvider,
router,
store,
render: h => h(App),
created() {
// execute getCurrentUser query
this.$store.dispatch("getCurrentUser");
}
}).$mount("#app");
- We can now test if it works.


42. Redirect Home upon Signin with Watcher 8min
- We are going to modify the
Signincomponent to redirect to home page if the data received from theusergetter changes.
client\src\components\Auth\Signin.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signin Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>Welcome Back!</h1>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="secondary" dark>
<v-container>
<v-form @submit.prevent="handleSigninUser">
<v-layout row>
<v-flex xs12>
<v-text-field v-model="username" prepend-icon="face" label="Username" type="text" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field v-model="password" prepend-icon="extension" label="Password" type="password" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn color="accent" type="submit">Signin</v-btn>
<h3>Don't have an account?
<router-link to="/signup">Signup</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signin",
data() {
return {
username: "",
password: ""
};
},
computed: {
...mapGetters(["user"])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
methods: {
handleSigninUser() {
this.$store.dispatch("signinUser", {
username: this.username,
password: this.password
});
}
}
};
</script>
- We can now test if it works.

- When the new user data is received it is redirected to the main page.

43. Change Navbar for Signed-in User 9min
- We are going to modify the
App.vuedocument to put theNavbar routesbased on the user authenticated or not.
src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile v-if="user">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn flat to="/profile" v-if="user">
<v-icon class="hidden-sm-only" left>account_box</v-icon>
<v-badge right color="blue darken-2">
<!-- <span slot="badge"></span> -->
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn flat v-if="user">
<v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view/>
</transition>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
...mapGetters(["user"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
- Before signing in:


- After signing in:


44. Create Signout Action 6min
- we are going to modify the
store.jsdocument to create amutationand anactionto be able tosign outa user.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import router from "./router";
import { defaultClient as apolloClient } from "./main";
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER } from "./queries";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setUser: (state, payload) => {
state.user = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
},
clearUser: state => (state.user = null)
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false);
// Add user data to state
commit("setUser", data.getCurrentUser);
console.log(data.getCurrentUser);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
signinUser: (_, payload) => {
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
localStorage.setItem("token", data.signinUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser");
// remove token in localStorage
localStorage.setItem("token", "");
// end session
await apolloClient.resetStore();
// redirect home - kick users out of private pages (i.e. profile)
router.push("/");
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading
}
});
- We are also going to modify the
App.vuepage to include the functionaly tosign outa user.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile v-if="user" @click="handleSignoutUser">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn flat to="/profile" v-if="user">
<v-icon class="hidden-sm-only" left>account_box</v-icon>
<v-badge right color="blue darken-2">
<!-- <span slot="badge"></span> -->
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn flat v-if="user" @click="handleSignoutUser">
<v-icon class="hidden-sm-only" left>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view/>
</transition>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
...mapGetters(["user"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>




45. Protected Routes and Clearing Malformed Tokens 5min
- We are going to create the
AuthGuard.jsdocument that will be used to protect the routes that cannot be accessed if the user is not authenticated.
client\src\AuthGuard.js
import store from "./store";
export default (to, from, next) => {
if (!store.getters.user) {
next({
path: "/signin"
});
} else {
next();
}
};
- We are also going to modify the
router.jsdocument to use the newAuthGuard.jsdocument.
client\src\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
import AddPost from "./components/Posts/AddPost.vue";
import Posts from "./components/Posts/Posts.vue";
import Profile from "./components/Auth/Profile.vue";
import Signin from "./components/Auth/Signin.vue";
import Signup from "./components/Auth/Signup.vue";
import AuthGuard from "./AuthGuard";
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/posts",
name: "Posts",
component: Posts
},
{
path: "/post/add",
name: "AddPost",
component: AddPost
},
{
path: "/profile",
name: "Profile",
component: Profile,
beforeEnter: AuthGuard
},
{
path: "/signin",
name: "Signin",
component: Signin
},
{
path: "/Signup",
name: "Signup",
component: Signup
}
]
});
- We are going to modify the
store.jsdocument to remobe the token from localStorage before trying to sign in.
client\src\store.js
import Vue from "vue";
import Vuex from "vuex";
import router from "./router";
import { defaultClient as apolloClient } from "./main";
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER } from "./queries";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload;
},
setUser: (state, payload) => {
state.user = payload;
},
setLoading: (state, payload) => {
state.loading = payload;
},
clearUser: state => (state.user = null)
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false);
// Add user data to state
commit("setUser", data.getCurrentUser);
console.log(data.getCurrentUser);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
getPosts: ({ commit }) => {
commit("setLoading", true);
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts);
commit("setLoading", false);
})
.catch(err => {
commit("setLoading", false);
console.error(err);
});
},
signinUser: (_, payload) => {
// clear token to prevent errors (if malformed or token expired)
localStorage.setItem("token", "");
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
localStorage.setItem("token", data.signinUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser");
// remove token in localStorage
localStorage.setItem("token", "");
// end session
await apolloClient.resetStore();
// redirect home - kick users out of private pages (i.e. profile)
router.push("/");
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading
}
});


Section 9: Error Handling and Form Validation 0 / 6|43min
46. Adding Global Form Alert Component 9min
- We need to modify the
store.jsdocument to include theerrorstate.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER } from "./queries"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
// clear token to prevent errors (if malformed or token expired)
localStorage.setItem("token", "")
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error
}
})
- We are going to create the new
Sharedfolder from theclient\src\componentsand theFormAlert.vuecomponent documnent.
client\src\components\Shared\FormAlert.vue
<template>
<v-alert type="error" :value="true" transition="scale-transition" dismissible>
<h3>{{message}}</h3>
</v-alert>
</template>
<script>
export default {
props: ["message"]
};
</script>
= We need to modify the main.js document to make the FormAlert component globally accesible.
client\src\main.js
import "@babel/polyfill"
import Vue from "vue"
import "./plugins/vuetify"
import App from "./App.vue"
import router from "./router"
import store from "./store"
import ApolloClient from "apollo-boost"
import VueApollo from "vue-apollo"
import FormAlert from "./components/Shared/FormAlert"
// Register Global Component
Vue.component("form-alert", FormAlert)
Vue.use(VueApollo)
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql",
// include auth token with requests made to backend
fetchOptions: {
credentials: "include"
},
request: operation => {
// if no token with key of 'token' in localStorage, add it
if (!localStorage.token) {
localStorage.setItem("token", "")
}
// operation adds the token to an authorization header, which is sent to backend
operation.setContext({
headers: {
authorization: localStorage.getItem("token")
}
})
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError)
}
if (graphQLErrors) {
for (let err of graphQLErrors) {
console.dir(err)
}
}
}
})
const apolloProvider = new VueApollo({ defaultClient })
Vue.config.productionTip = false
new Vue({
apolloProvider,
router,
store,
render: h => h(App),
created() {
// execute getCurrentUser query
this.$store.dispatch("getCurrentUser")
}
}).$mount("#app")
- We need to modify the
Signin.vuedocument page to use the newFormAlertcomponent and the newErrorstate.
client\src\components\Auth\Signin.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Signin Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1>Welcome Back!</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout v-if="error" row wrap>
<v-flex xs12 sm6 offset-sm3>
<form-alert :message="error.message"></form-alert>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-card color="secondary" dark>
<v-container>
<v-form @submit.prevent="handleSigninUser">
<v-layout row>
<v-flex xs12>
<v-text-field v-model="username" prepend-icon="face" label="Username" type="text" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field v-model="password" prepend-icon="extension" label="Password" type="password" required></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn color="accent" type="submit">Signin</v-btn>
<h3>Don't have an account?
<router-link to="/signup">Signup</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signin",
data() {
return {
username: "",
password: ""
};
},
computed: {
...mapGetters(["error", "user"])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
methods: {
handleSigninUser() {
this.$store.dispatch("signinUser", {
username: this.username,
password: this.password
});
}
}
};
</script>
- We can test if the new
FormAlertcomponent works properly.

47. Add Loading Spinner to Signin Button 3min
- We need to modify the
Signin.vuepage document to include aVuetifyloading spinner.
client\src\components\Auth\Signin.vue
<template>
<v-container
text-xs-center
mt-5
pt-5
>
<!-- Signin Title -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<h1>Welcome Back!</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout
v-if="error"
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<form-alert :message="error.message"></form-alert>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<v-card
color="secondary"
dark
>
<v-container>
<v-form @submit.prevent="handleSigninUser">
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="username"
prepend-icon="face"
label="Username"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="password"
prepend-icon="extension"
label="Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
color="accent"
type="submit"
>
<span
slot="loader"
class="custom-loader"
>
<v-icon light>cached</v-icon>
</span>
Signin</v-btn>
<h3>Don't have an account?
<router-link to="/signup">Signup</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signin",
data() {
return {
username: "",
password: ""
};
},
computed: {
...mapGetters(["loading", "error", "user"])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
methods: {
handleSigninUser() {
this.$store.dispatch("signinUser", {
username: this.username,
password: this.password
});
}
}
};
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
- We need to test if it is working.


48. Form Validation with Vuetify in Signin Component 7min
- We need to modify the
Signin.vuepage document to include theVuetifyform validation.
client\src\components\Auth\Signin.vue
<template>
<v-container
text-xs-center
mt-5
pt-5
>
<!-- Signin Title -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<h1>Welcome Back!</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout
v-if="error"
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<form-alert :message="error.message"></form-alert>
</v-flex>
</v-layout>
<!-- Signin Form -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<v-card
color="secondary"
dark
>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleSigninUser"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="usernameRules"
v-model="username"
prepend-icon="face"
label="Username"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="passwordRules"
v-model="password"
prepend-icon="extension"
label="Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid"
color="accent"
type="submit"
>
<span
slot="loader"
class="custom-loader"
>
<v-icon light>cached</v-icon>
</span>
Signin</v-btn>
<h3>Don't have an account?
<router-link to="/signup">Signup</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signin",
data() {
return {
isFormValid: true,
username: "",
password: "",
usernameRules: [
// Check if username in input
username => !!username || "Username is required",
// Make sure username is less than 10 characters
username =>
username.length < 10 || "Username must be less than 10 characters"
],
passwordRules: [
password => !!password || "Password is required",
// Make sure password is at least 4 characters
password =>
password.length >= 4 || "Password must be at least 4 characters"
]
};
},
computed: {
...mapGetters(["loading", "error", "user"])
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
methods: {
handleSigninUser() {
if (this.$refs.form.validate()) {
this.$store.dispatch("signinUser", {
username: this.username,
password: this.password
});
}
}
}
};
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
- We need to check if it works.




49. Show AuthSnackbar on Signin / Signup 5min
- We are going to modify the
App.vuemain page document to add aAuthSnackbarVuetify component that will be shown when the user authenticates.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
>
<!-- <span slot="badge"></span> -->
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
sideNav: false,
authSnackbar: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
}
},
computed: {
...mapGetters(["user"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
- We are going to test if it works.

50. Handle Authentication Errors, Show Auth Error Snackbar 9min
- We need to modify the
server.jsdocument to format the error that the Apollo Server sends.
server.js
const { ApolloServer, AuthenticationError } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Verify JWT Token passed from client
const getUser = async token => {
if (token) {
try {
return await jwt.verify(token, process.env.SECRET);
} catch (err) {
throw new AuthenticationError(
"Your session has ended. Please sign in again."
);
}
}
};
// Create Apollo/GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: error => ({
name: error.name,
message: error.message.replace("Context creation failed:", "")
}),
context: async ({ req }) => {
const token = req.headers["authorization"];
return { User, Post, currentUser: await getUser(token) };
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We are going to modify the
store.jsdocument to include the newAuthErrorstate.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER } from "./queries"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload;
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
// clear token to prevent errors (if malformed or token expired)
localStorage.setItem("token", "")
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We are going to modify the
main.jsdocument to update theAuthErrorstate when there is an authentication error.
client\src\main.js
import "@babel/polyfill"
import Vue from "vue"
import "./plugins/vuetify"
import App from "./App.vue"
import router from "./router"
import store from "./store"
import ApolloClient from "apollo-boost"
import VueApollo from "vue-apollo"
import FormAlert from "./components/Shared/FormAlert"
// Register Global Component
Vue.component("form-alert", FormAlert)
Vue.use(VueApollo)
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "http://localhost:4000/graphql",
// include auth token with requests made to backend
fetchOptions: {
credentials: "include"
},
request: operation => {
// if no token with key of 'token' in localStorage, add it
if (!localStorage.token) {
localStorage.setItem("token", "")
}
// operation adds the token to an authorization header, which is sent to backend
operation.setContext({
headers: {
authorization: localStorage.getItem("token")
}
})
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError)
}
if (graphQLErrors) {
for (let err of graphQLErrors) {
console.dir(err)
if (err.name === "AuthenticationError") {
// set auth error in state (to show in snackbar)
store.commit("setAuthError", err);
// signout user (to clear token)
store.dispatch("signoutUser");
}
}
}
}
})
const apolloProvider = new VueApollo({ defaultClient })
Vue.config.productionTip = false
new Vue({
apolloProvider,
router,
store,
render: h => h(App),
created() {
// execute getCurrentUser query
this.$store.dispatch("getCurrentUser")
}
}).$mount("#app")
- We are going to modify the
App.vuemain page document to add anotherAuthSnackbarVuetify component that will be shown when there is any error while the user authenticates.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
>
<!-- <span slot="badge"></span> -->
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
}
},
computed: {
...mapGetters(["authError", "user"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
- We need to check if it works properly.



51. Create Signup Form and Signup User Action 11min
- We need to modify the
store.jsto add the newsignupUseraction.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER, SIGNUP_USER } from "./queries"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload;
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to update the
Signup.vuepage document to include all the functionality to manage theSignup.
client\src\components\Auth\Signup.vue
<template>
<v-container
text-xs-center
mt-5
pt-5
>
<!-- Signup Title -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<h1>Get Started Here</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout
v-if="error"
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<form-alert :message="error.message"></form-alert>
</v-flex>
</v-layout>
<!-- Signup Form -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<v-card
color="accent"
dark
>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleSignupUser"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="usernameRules"
v-model="username"
prepend-icon="face"
label="Username"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="emailRules"
v-model="email"
prepend-icon="email"
label="Email"
type="email"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="passwordRules"
v-model="password"
prepend-icon="extension"
label="Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="passwordRules"
v-model="passwordConfirmation"
prepend-icon="gavel"
label="Confirm Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid || loading"
color="info"
type="submit"
>
<span
slot="loader"
class="custom-loader"
>
<v-icon light>cached</v-icon>
</span>
Signup</v-btn>
<h3>Already have an account?
<router-link to="/signin">Signin</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signup",
data() {
return {
isFormValid: true,
username: "",
email: "",
password: "",
passwordConfirmation: "",
usernameRules: [
username => !!username || "Username is required",
username =>
username.length < 10 || "Username cannot be more than 10 characters"
],
emailRules: [
email => !!email || "Email is required",
email => /.@+./.test(email) || "Email must be valid"
],
passwordRules: [
password => !!password || "Password is required",
password =>
password.length >= 4 || "Password must be at least 4 characters",
confirmation => confirmation === this.password || "Passwords must match"
]
};
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
computed: {
...mapGetters(["loading", "error", "user"])
},
methods: {
handleSignupUser() {
if (this.$refs.form.validate()) {
this.$store.dispatch("signupUser", {
username: this.username,
email: this.email,
password: this.password
});
}
}
}
};
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
- We need to ensure it works properly.





Section 10: Add Post / Infinite Scroll Components 0 / 6|58min
52. Make Add Post Form 8min
- We are going to modify the
posts\add.vuepage document to include the form that it's going to allow us to create a newpost.
client\pages\posts\add.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Add Post Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1 class="primary--text">Add Post</h1>
</v-flex>
</v-layout>
<!-- Add Post Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-form v-model="isFormValid" lazy-validation ref="form" @submit.prevent="handleAddPost">
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="titleRules" v-model="title" label="Post Title" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="imageRules" v-model="imageUrl" label="Image URL" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img :src="imageUrl" height="300px">
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select v-model="categories" :rules="categoriesRules" :items="['Art', 'Education', 'Travel', 'Photography', 'Technology']" multiple label="Categories"></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea :rules="descRules" v-model="description" label="Description" type="text" required></v-textarea>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn :loading="loading" :disabled="!isFormValid || loading" color="info" type="submit">
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
Submit</v-btn>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "AddPost",
data() {
return {
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
}
};
</script>
- We need to ensure it works correctly





53. Create and Execute addPost Action / Mutation 10min
- We need to modify the
queries.jsdocument to include theaddPostmutation.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We need to modify the
store.jsstore document to include theaddPostaction.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER, SIGNUP_USER, ADD_POST } from "./queries"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to modify the
AddPost.vuepage document to include the call to theaddPostaction.
client\src\components\Posts\AddPost.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Add Post Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1 class="primary--text">Add Post</h1>
</v-flex>
</v-layout>
<!-- Add Post Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-form v-model="isFormValid" lazy-validation ref="form" @submit.prevent="handleAddPost">
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="titleRules" v-model="title" label="Post Title" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="imageRules" v-model="imageUrl" label="Image URL" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img :src="imageUrl" height="300px">
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select v-model="categories" :rules="categoriesRules" :items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']" multiple label="Categories"></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea :rules="descRules" v-model="description" label="Description" type="text" required></v-textarea>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn :loading="loading" :disabled="!isFormValid || loading" color="info" type="submit">
<span slot="loader" class="custom-loader">
<v-icon light>cached</v-icon>
</span>
Submit</v-btn>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "AddPost",
data() {
return {
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["loading", "user"])
},
methods: {
handleAddPost() {
if (this.$refs.form.validate()) {
// add post action
this.$store.dispatch("addPost", {
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description,
creatorId: this.user._id
});
this.$router.push("/");
}
}
}
};
</script>
- We need to update the
router.jsdocument to forbid the access to thepostAddpage if the user is not authenticated using theAuthGuardmiddleware.
client\src\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
import AddPost from "./components/Posts/AddPost.vue";
import Posts from "./components/Posts/Posts.vue";
import Profile from "./components/Auth/Profile.vue";
import Signin from "./components/Auth/Signin.vue";
import Signup from "./components/Auth/Signup.vue";
import AuthGuard from "./AuthGuard";
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/posts",
name: "Posts",
component: Posts
},
{
path: "/post/add",
name: "AddPost",
component: AddPost,
beforeEnter: AuthGuard
},
{
path: "/profile",
name: "Profile",
component: Profile,
beforeEnter: AuthGuard
},
{
path: "/signin",
name: "Signin",
component: Signin
},
{
path: "/Signup",
name: "Signup",
component: Signup
}
]
});
- We need to ensure it works.


54. Update and Optimistic Response for addPost Mutation 11min
- We need to modify the
store.jsstore document to update the cache with the new post so that it is included immediately.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import { GET_CURRENT_USER, GET_POSTS, SIGNIN_USER, SIGNUP_USER, ADD_POST } from "./queries"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to ensure it works.


55. Infinite Scroll on Posts Page; Add typeDef, Resolver, and Query 8min
- We need to modify the
typeDefs.gqldocument to include the newinfiniteScrollPostsquery.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
type Query {
getCurrentUser: User
getPosts: [Post]
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We need to modify the
resolvers.jsdocument to include the newinfiniteScrollPostsquery.
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We are going to modify the
queries.jsdocument to include the newinfiniteScrollPostsquery.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We need to ensure the new
infiniteScrollPostsworks properly.

query
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
variables
{
"pageNum": 1,
"pageSize": 3
}
response
{
"data": {
"infiniteScrollPosts": {
"hasMore": true,
"posts": [
{
"_id": "5d4fa77cfe7af92694bf3cae",
"title": "Abstract Painting",
"imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Vassily_Kandinsky%2C_1939_-_Composition_10.jpg/1024px-Vassily_Kandinsky%2C_1939_-_Composition_10.jpg",
"categories": [
"Art"
],
"description": "Nice painting",
"likes": 0,
"createdDate": "Sun Aug 11 2019 06:28:28 GMT+0100 (Irish Standard Time)",
"messages": [],
"createdBy": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
}
},
{
"_id": "5d4fa31dcb16795b68483e10",
"title": "At the Beach",
"imageUrl": "https://images.pexels.com/photos/1139541/pexels-photo-1139541.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
"categories": [
"Photography",
"Travel"
],
"description": "A nice photo of the waves",
"likes": 0,
"createdDate": "Sun Aug 11 2019 06:09:49 GMT+0100 (Irish Standard Time)",
"messages": [],
"createdBy": {
"_id": "5d35390f58729c10f875d41c",
"username": "Fofo",
"avatar": "http://gravatar.com/avatar/d7ec7a6a7980721f40631794b6e691f8?d=identicon"
}
},
{
"_id": "5d0e10be7dd57444d0607790",
"title": "Tasty coffee",
"imageUrl": "https://images.pexels.com/photos/374757/pexels-photo-374757.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
"categories": [
"Art",
"Food"
],
"description": "Some nice coffee artwork",
"likes": 0,
"createdDate": "Sat Jun 22 2019 12:27:58 GMT+0100 (Irish Standard Time)",
"messages": [],
"createdBy": {
"_id": "5d36a9e007bced421c3a2031",
"username": "John",
"avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon"
}
}
]
}
}
}

query
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
variables
{
"pageNum": 2,
"pageSize": 3
}
response
{
"data": {
"infiniteScrollPosts": {
"hasMore": false,
"posts": [
{
"_id": "5d0e0fdc7dd57444d060778d",
"title": "Credenza",
"imageUrl": "https://images.crateandbarrel.com/is/image/Crate/ClybournIICredenza3QF16/?$web_zoom_furn_av$&180802085137&wid=1008&hei=567",
"categories": [
"Furniture"
],
"description": "A piece of furniture I want to buy",
"likes": 0,
"createdDate": "Sat Jun 22 2019 12:24:12 GMT+0100 (Irish Standard Time)",
"messages": [],
"createdBy": {
"_id": "5d36a9e007bced421c3a2031",
"username": "John",
"avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon"
}
},
{
"_id": "5d04d377deb8673e8c38fc2f",
"title": "Mona lisa",
"imageUrl": "https://images.duckduckgo.com/iu/?u=http%3A%2F%2Fwww.readingpublicmuseum.org%2Fexhibit_secrets-of-mona-lisa-4.jpg&f=1",
"categories": [
"Art"
],
"description": "A painting",
"likes": 0,
"createdDate": "Sat Jun 15 2019 12:16:07 GMT+0100 (Irish Standard Time)",
"messages": [],
"createdBy": {
"_id": "5d36a9e007bced421c3a2031",
"username": "John",
"avatar": "http://gravatar.com/avatar/61409aa1fd47d4a5332de23cbf59a36f?d=identicon"
}
}
]
}
}
}

query
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
variables
{
"pageNum": 3,
"pageSize": 3
}
response
{
"data": {
"infiniteScrollPosts": {
"hasMore": false,
"posts": []
}
}
}
56. Add Infinite Scroll Functionality on Client 10min
- We need to modify the
Posts.vuepage document to include all the posts based on the newinfiniteScrollPostsquery.
client\src\components\Posts\Posts.vue
<template>
<v-container
text-xs-center
v-if="infiniteScrollPosts"
>
<div
v-for="post in infiniteScrollPosts.posts"
:key="post._id"
>
<img
:src="post.imageUrl"
height="100px"
alt=""
/>
<h3>{{post.title}}</h3>
</div>
<v-btn
color="info"
@click="showMorePosts"
v-if="showMoreEnabled"
>Fetch More</v-btn>
</v-container>
</template>
<script>
import { INFINITE_SCROLL_POSTS } from "../../queries";
const pageSize = 2;
export default {
name: "Posts",
data() {
return {
pageNum: 1,
showMoreEnabled: true
};
},
apollo: {
infiniteScrollPosts: {
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize
}
}
},
methods: {
showMorePosts() {
this.pageNum += 1;
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log("previous result", prevResult.infiniteScrollPosts.posts);
console.log("fetch more result", fetchMoreResult);
const newPosts = fetchMoreResult.infiniteScrollPosts.posts;
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore;
this.showMoreEnabled = hasMore;
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
};
}
});
}
}
};
</script>
- We need to test if it works.



57. Add Grid Layout / Cards for Each Post in Posts Component 10min
- We are going to improve the
Posts.vuedocument using aGrid Layoutand aCardVuetify component for each post.
client\src\components\Posts\Posts.vue
<template>
<v-container fluid grid-list-xl>
<!-- Post Cards -->
<v-layout row wrap v-if="infiniteScrollPosts">
<v-flex xs12 sm6 v-for="post in infiniteScrollPosts.posts" :key="post._id">
<v-card hover>
<v-card-media :src="post.imageUrl" height="30vh" lazy></v-card-media>
<v-card-actions>
<v-card-title primary>
<div>
<div class="headline">{{post.title}}</div>
<span class="grey--text">{{post.likes}} likes - {{post.messages.length}} comments</span>
</div>
</v-card-title>
<v-spacer></v-spacer>
<v-btn @click="showPostCreator = !showPostCreator" icon>
<v-icon>{{`keyboard_arrow_${showPostCreator ? 'up' : 'down'}`}}</v-icon>
</v-btn>
</v-card-actions>
<!-- Post Creator Tile -->
<v-slide-y-transition>
<v-card-text v-show="showPostCreator" class="grey lighten-4">
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="post.createdBy.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="text--primary">{{post.createdBy.username}}</v-list-tile-title>
<v-list-tile-subtitle class="font-weight-thin">Added {{post.createdDate}}</v-list-tile-subtitle>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon ripple>
<v-icon color="grey lighten-1">info</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-card-text>
</v-slide-y-transition>
</v-card>
</v-flex>
</v-layout>
<!-- Fetch More Button -->
<v-layout v-if="showMoreEnabled" column>
<v-flex xs12>
<v-layout justify-center row>
<v-btn color="info" @click="showMorePosts">Fetch More</v-btn>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { INFINITE_SCROLL_POSTS } from "../../queries";
const pageSize = 2;
export default {
name: "Posts",
data() {
return {
pageNum: 1,
showMoreEnabled: true,
showPostCreator: false
};
},
apollo: {
infiniteScrollPosts: {
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize
}
}
},
methods: {
showMorePosts() {
this.pageNum += 1;
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log("previous result", prevResult.infiniteScrollPosts.posts);
console.log("fetch more result", fetchMoreResult);
const newPosts = fetchMoreResult.infiniteScrollPosts.posts;
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore;
this.showMoreEnabled = hasMore;
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
};
}
});
}
}
};
</script>
- We need to test if it works.





Section 11: Post Component 0 / 7|44min
58. Create Post Component and Route 5min
- We need to modify the
router.jsdocument to include a new route for the newPostComponent
client\src\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
import AddPost from "./components/Posts/AddPost.vue";
import Posts from "./components/Posts/Posts.vue";
import Post from "./components/Posts/Post.vue";
import Profile from "./components/Auth/Profile.vue";
import Signin from "./components/Auth/Signin.vue";
import Signup from "./components/Auth/Signup.vue";
import AuthGuard from "./AuthGuard";
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/posts",
name: "Posts",
component: Posts
},
{
path: "/posts/:postId",
name: "Post",
component: Post,
props: true
},
{
path: "/post/add",
name: "AddPost",
component: AddPost,
beforeEnter: AuthGuard
},
{
path: "/profile",
name: "Profile",
component: Profile,
beforeEnter: AuthGuard
},
{
path: "/signin",
name: "Signin",
component: Signin
},
{
path: "/Signup",
name: "Signup",
component: Signup
}
]
});
- We are going to create the new
Posts\Post.vuedocument with thePostComponent.
client\src\components\Posts\Post.vue
<template>
<v-container>
<h1>Post {{postId}}</h1>
</v-container>
</template>
<script>
export default {
name: "Post",
props: ['postId']
};
</script>
- We are going to modify the
Home.vuecomponent document to modify the carousel component to allow it to access thePostinformation, using the newPostcomponent, when it is clicked.
client\src\components\Home.vue
<template>
<v-container text-xs-center>
<v-layout row>
<v-dialog v-model="loading" persistent fullscreen>
<v-container fill-height>
<v-layout row justify-center align-center>
<v-progress-circular indeterminate :size="70" :width="7" color="secondary"></v-progress-circular>
</v-layout>
</v-container>
</v-dialog>
</v-layout>
<v-flex xs12>
<v-carousel v-if="!loading && posts.length > 0" v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in posts" :key="post._id" :src="post.imageUrl" @click.native="goToPost(post._id)">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "home",
created() {
this.handleGetCarouselPosts();
},
computed: {
...mapGetters(["loading", "posts"])
},
methods: {
handleGetCarouselPosts() {
// reach out to Vuex store, fire action that gets posts for carousel
this.$store.dispatch("getPosts");
},
goToPost(postId) {
this.$router.push(`/posts/${postId}`);
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>
- We are going to modify the
Posts.vuecomponent document to modify the image component to allow it to access thePostinformation, using the newPostcomponent, when it is clicked.
client\src\components\Posts\Posts.vue
<template>
<v-container fluid grid-list-xl>
<!-- Post Cards -->
<v-layout row wrap v-if="infiniteScrollPosts">
<v-flex xs12 sm6 v-for="post in infiniteScrollPosts.posts" :key="post._id">
<v-card hover>
<v-img @click.native="goToPost(post._id)" :src="post.imageUrl" height="30vh" lazy></v-img>
<v-card-actions>
<v-card-title primary>
<div>
<div class="headline">{{post.title}}</div>
<span class="grey--text">{{post.likes}} likes - {{post.messages.length}} comments</span>
</div>
</v-card-title>
<v-spacer></v-spacer>
<v-btn @click="showPostCreator = !showPostCreator" icon>
<v-icon>{{`keyboard_arrow_${showPostCreator ? 'up' : 'down'}`}}</v-icon>
</v-btn>
</v-card-actions>
<!-- Post Creator Tile -->
<v-slide-y-transition>
<v-card-text v-show="showPostCreator" class="grey lighten-4">
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="post.createdBy.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="text--primary">{{post.createdBy.username}}</v-list-tile-title>
<v-list-tile-sub-title class="font-weight-thin">Added {{post.createdDate}}</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon ripple>
<v-icon color="grey lighten-1">info</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-card-text>
</v-slide-y-transition>
</v-card>
</v-flex>
</v-layout>
<!-- Fetch More Button -->
<v-layout v-if="showMoreEnabled" column>
<v-flex xs12>
<v-layout justify-center row>
<v-btn color="info" @click="showMorePosts">Fetch More</v-btn>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { INFINITE_SCROLL_POSTS } from "../../queries";
const pageSize = 2;
export default {
name: "Posts",
data() {
return {
pageNum: 1,
showMoreEnabled: true,
showPostCreator: false
};
},
apollo: {
infiniteScrollPosts: {
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize
}
}
},
methods: {
showMorePosts() {
this.pageNum += 1;
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log("previous result", prevResult.infiniteScrollPosts.posts);
console.log("fetch more result", fetchMoreResult);
const newPosts = fetchMoreResult.infiniteScrollPosts.posts;
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore;
this.showMoreEnabled = hasMore;
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
};
}
});
},
goToPost(postId) {
this.$router.push(`/posts/${postId}`);
}
}
};
</script>
- We are going to check if it works.




59. Create and Execute getPost Query 7min
- We need to modify the
typeDefs.gqldocument to include the newgetPostQuery.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
type Query {
getCurrentUser: User
getPosts: [Post]
getPost(postId: ID!): Post!
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We need to modify the
resolvers.jsdocument to include the newgetPostQuery.
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We need to modify the
client\src\queries.jsto include theGET_POSTQuery.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We are going to modify the
Posts\Post.vuepage document to call thegetPostQuery to obtain the data about the post.
client\src\components\Posts\Post.vue
<template>
<v-container v-if="getPost">
<h1>{{getPost.title}}</h1>
</v-container>
</template>
<script>
import { GET_POST } from "../../queries";
export default {
name: "Post",
props: ['postId'],
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
}
};
</script>
- We need to test if it works:



60. Build out Post Card in Post Component 8min
- We are going to modify the
Posts\Post.vuepage document to calput all the nformation about the post.
client\src\components\Posts\Post.vue
<template>
<v-container v-if="getPost" class="mt-3" flexbox center>
<!-- Post Card -->
<v-layout row wrap>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn large icon v-if="user">
<v-icon large color="grey">favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon @click="goToPreviousPage" color="info" large>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-card-media @click="toggleImageDialog" slot="activator" :src="getPost.imageUrl" id="post__image"></v-card-media>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-card-media :src="getPost.imageUrl" height="80vh"></v-card-media>
</v-card>
</v-dialog>
<v-card-text>
<span v-for="(category, index) in getPost.categories" :key="index">
<v-chip class="mb-3" color="accent" text-color="white">{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
import { GET_POST } from "../../queries";
export default {
name: "Post",
props: ['postId'],
data() {
return {
dialog: false
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user"])
},
methods: {
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We need to test if it works.




61. Add Messages ## Section to Post Component 6min
- We are going to modify the
Posts\Post.vuepage document to include theMessagessection
client\src\components\Posts\Post.vue
<template>
<v-container v-if="getPost" class="mt-3" flexbox center>
<!-- Post Card -->
<v-layout row wrap>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn large icon v-if="user">
<v-icon large color="grey">favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon @click="goToPreviousPage" color="info" large>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-img @click="toggleImageDialog" slot="activator" :src="getPost.imageUrl" id="post__image"></v-img>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-img :src="getPost.imageUrl" height="80vh"></v-img>
</v-card>
</v-dialog>
<v-card-text>
<span v-for="(category, index) in getPost.categories" :key="index">
<v-chip class="mb-3" color="accent" text-color="white">{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
<!-- Messages Section -->
<div class="mt-3">
<!-- Message Input -->
<v-layout class="mb-3" v-if="user">
<v-flex xs12>
<v-form>
<v-layout row>
<v-flex xs12>
<v-text-field clearable append-outer-icon="send" label="Add Message" type="text" prepend-icon="email" required></v-text-field>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
<!-- Messages -->
<v-layout row wrap>
<v-flex xs12>
<v-list subheader two-line>
<v-subheader>Messages ({{getPost.messages.length}})</v-subheader>
<template v-for="message in getPost.messages">
<v-divider :key="message._id"></v-divider>
<v-list-tile avatar inset :key="message.title">
<v-list-tile-avatar>
<img :src="message.messageUser.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{message.messageBody}}
</v-list-tile-title>
<v-list-tile-sub-title>
{{message.messageUser.username}}
<span class="grey--text text--lighten-1 hidden-xs-only">{{message.messageDate}}</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action class='hidden-xs-only'>
<v-icon color="grey">chat_bubble</v-icon>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-flex>
</v-layout>
</div>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
import { GET_POST } from "../../queries";
export default {
name: "Post",
props: ['postId'],
data() {
return {
dialog: false
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user"])
},
methods: {
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We can test if it works but as we don't have messages yet we can't see much.

62. Create addPostMessage Mutation 6min
- We need to modify the
typeDefs.gqldocument to include the newaddPostMessageMutation
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
type Query {
getCurrentUser: User
getPosts: [Post]
getPost(postId: ID!): Post!
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We need to modify the
resolvers.jsdocument to include the newaddPostMessageMutation
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We need to modify the
queries.jsdocument to include the newADD_POST_MESSAGEmutation.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We have to ensure there are not errors at the terminal
[client] WAIT Compiling...6:03:37 PM
[server] [nodemon] restarting due to changes...
[client]
[server] [nodemon] restarting due to changes...
[server] [nodemon] restarting due to changes...
[client] 40% building 0/1 modules 1 active ...he-ultimate-guide\client\src\queries.js[nodemon] starting `node server.js`
98% after emitting DONE Compiled successfully in 828ms6:03:38 PM
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.240.205:8080/
[client]
[server] Server listening on http://localhost:4000/
[server] DB connected
63. Perform addPostMessage in Post Component 8min
- We need to modify the
Posts\Post.vuepage component to add the call to the newaddPostMessagemutation.
client\src\components\Posts\Post.vue
<template>
<v-container
v-if="getPost"
class="mt-3"
flexbox
center
>
<!-- Post Card -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn
large
icon
v-if="user"
>
<v-icon
large
color="grey"
>favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon
@click="goToPreviousPage"
color="info"
large
>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-img
@click="toggleImageDialog"
slot="activator"
:src="getPost.imageUrl"
id="post__image"
></v-img>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-img
:src="getPost.imageUrl"
height="80vh"
></v-img>
</v-card>
</v-dialog>
<v-card-text>
<span
v-for="(category, index) in getPost.categories"
:key="index"
>
<v-chip
class="mb-3"
color="accent"
text-color="white"
>{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
<!-- Messages Section -->
<div class="mt-3">
<!-- Message Input -->
<v-layout
class="mb-3"
v-if="user"
>
<v-flex xs12>
<v-form @submit.prevent="handleAddPostMessage">
<v-layout row>
<v-flex xs12>
<v-text-field
v-model="messageBody"
clearable
:append-outer-icon="messageBody && 'send'"
label="Add Message"
type="text"
@click:append-outer="handleAddPostMessage"
prepend-icon="email"
required
></v-text-field>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
<!-- Messages -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-list
subheader
two-line
>
<v-subheader>Messages ({{getPost.messages.length}})</v-subheader>
<template v-for="message in getPost.messages">
<v-divider :key="message._id"></v-divider>
<v-list-tile
avatar
inset
:key="message.title"
>
<v-list-tile-avatar>
<img :src="message.messageUser.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{message.messageBody}}
</v-list-tile-title>
<v-list-tile-sub-title>
{{message.messageUser.username}}
<span class="grey--text text--lighten-1 hidden-xs-only">{{message.messageDate}}</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action class='hidden-xs-only'>
<v-icon color="grey">chat_bubble</v-icon>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-flex>
</v-layout>
</div>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
import { GET_POST, ADD_POST_MESSAGE } from "../../queries";
export default {
name: "Post",
props: ["postId"],
data() {
return {
dialog: false,
messageBody: ""
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user"])
},
methods: {
handleAddPostMessage() {
const variables = {
messageBody: this.messageBody,
userId: this.user._id,
postId: this.postId
};
this.$apollo
.mutate({
mutation: ADD_POST_MESSAGE,
variables,
update: (cache, { data: { addPostMessage } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.messages.unshift(addPostMessage);
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
console.log(data.addPostMessage);
})
.catch(err => console.error(err));
},
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We need to test if it works. We need to be authenticated to be able to create messages.


- Now we sign in with another user.


64. Add Validation for Message Input, Clear on Submit 5min
- We need to modify the
Posts\Post.vuepage component to add include Validation for Message Input .
client\src\components\Posts\Post.vue
- We need to check if it works properly.





Section 12: Like / Unlike Post 0 / 4|27min
65. Create typeDefs / resolvers / queries for Like / Unlike 8min
- We need to modify the
typeDefs.gqldocument to include theLikesFavesType and thelikePostandunlikePostMutations.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
# Likes for Post / Favorites for User
type LikesFaves {
likes: Int
favorites: [Post]
}
type Query {
getCurrentUser: User
getPosts: [Post]
getPost(postId: ID!): Post!
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
likePost(postId: ID!, username: String!): LikesFaves!
unlikePost(postId: ID!, username: String!): LikesFaves!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We need to modify the
resolvers.jsdocument to include thelikePostandunlikePostMutations
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
likePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add 1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: 1 } },
{ new: true }
);
// Find User, add id of post to its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $addToSet: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
unlikePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add -1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: -1 } },
{ new: true }
);
// Find User, remove id of post from its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $pull: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We need to modify the
client\src\queries.jsdocument to include theLIKE_POSTandUNLIKE_POSTmutations.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
export const LIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
likePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
export const UNLIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
unlikePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- Ensure there are no error when running the application.
rver] [nodemon] starting `node server.js`
[client] INFO Starting development server...
[server] Server listening on http://localhost:4000/
[server] DB connected
[client] 98% after emitting CopyPlugin DONE Compiled successfully in 63825ms6:33:03 AM
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.1.64:8080/
[client]
[client] Note that the development build is not optimized.
[client] To create a production build, run npm run build.
[client]
66. Firing Like / Unlike Post Mutations from Client 9min
- We are going to modify the
Posts\Post.vuedocument to implement theLike/Unlikebutton.
client\src\components\Posts\Post.vue
<template>
<v-container
v-if="getPost"
class="mt-3"
flexbox
center
>
<!-- Post Card -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn
@click="handleLikePost"
large
icon
v-if="user"
>
<v-icon
large
color="grey"
>favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon
@click="goToPreviousPage"
color="info"
large
>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-img
@click="toggleImageDialog"
slot="activator"
:src="getPost.imageUrl"
id="post__image"
></v-img>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-img
:src="getPost.imageUrl"
height="80vh"
></v-img>
</v-card>
</v-dialog>
<v-card-text>
<span
v-for="(category, index) in getPost.categories"
:key="index"
>
<v-chip
class="mb-3"
color="accent"
text-color="white"
>{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
<!-- Messages Section -->
<div class="mt-3">
<!-- Message Input -->
<v-layout
class="mb-3"
v-if="user"
>
<v-flex xs12>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleAddPostMessage"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="messageRules"
v-model="messageBody"
clearable
:append-outer-icon="messageBody && 'send'"
label="Add Message"
type="text"
@click:append-outer="handleAddPostMessage"
prepend-icon="email"
required
></v-text-field>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
<!-- Messages -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-list
subheader
two-line
>
<v-subheader>Messages ({{getPost.messages.length}})</v-subheader>
<template v-for="message in getPost.messages">
<v-divider :key="message._id"></v-divider>
<v-list-tile
avatar
inset
:key="message.title"
>
<v-list-tile-avatar>
<img :src="message.messageUser.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{message.messageBody}}
</v-list-tile-title>
<v-list-tile-sub-title>
{{message.messageUser.username}}
<span class="grey--text text--lighten-1 hidden-xs-only">{{message.messageDate}}</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action class='hidden-xs-only'>
<v-icon :color="checkIfOwnMessage(message) ? 'accent' : 'grey'">chat_bubble</v-icon>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-flex>
</v-layout>
</div>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
import {
GET_POST,
ADD_POST_MESSAGE,
LIKE_POST,
UNLIKE_POST
} from "../../queries";
export default {
name: "Post",
props: ["postId"],
data() {
return {
dialog: false,
messageBody: "",
isFormValid: true,
messageRules: [
message => !!message || "Message is required",
message =>
message && message.length < 75 || "Message must be less than 75 characters"
]
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user"])
},
methods: {
handleLikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: LIKE_POST,
variables,
update: (cache, { data: { likePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes += 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.likePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleUnlikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: UNLIKE_POST,
variables,
update: (cache, { data: { unlikePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes -= 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.unlikePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleAddPostMessage() {
if (this.$refs.form.validate()) {
const variables = {
messageBody: this.messageBody,
userId: this.user._id,
postId: this.postId
};
this.$apollo
.mutate({
mutation: ADD_POST_MESSAGE,
variables,
update: (cache, { data: { addPostMessage } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.messages.unshift(addPostMessage);
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
this.$refs.form.reset();
console.log(data.addPostMessage);
})
.catch(err => console.error(err));
}
},
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
},
checkIfOwnMessage(message) {
return this.user && this.user._id === message.messageUser._id;
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We need to test if it works.




67. Add Logic for Toggling Like / Unlike Post 5min
- We need to modify the
store.jsstore document to include theuserFavoritesgetter.
client\src\store.js
- We are going to modify the
cPosts\Post.vuedocument to finish the implementation of theLike/Unlikebutton.
client\src\components\Posts\Post.vue
<template>
<v-container
v-if="getPost"
class="mt-3"
flexbox
center
>
<!-- Post Card -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn
@click="handleToggleLike"
large
icon
v-if="user"
>
<v-icon
large
:color="checkIfPostLiked(getPost._id) ? 'red' : 'grey'"
>favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon
@click="goToPreviousPage"
color="info"
large
>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-img
@click="toggleImageDialog"
slot="activator"
:src="getPost.imageUrl"
id="post__image"
></v-img>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-img
:src="getPost.imageUrl"
height="80vh"
></v-img>
</v-card>
</v-dialog>
<v-card-text>
<span
v-for="(category, index) in getPost.categories"
:key="index"
>
<v-chip
class="mb-3"
color="accent"
text-color="white"
>{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
<!-- Messages Section -->
<div class="mt-3">
<!-- Message Input -->
<v-layout
class="mb-3"
v-if="user"
>
<v-flex xs12>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleAddPostMessage"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="messageRules"
v-model="messageBody"
clearable
:append-outer-icon="messageBody && 'send'"
label="Add Message"
type="text"
@click:append-outer="handleAddPostMessage"
prepend-icon="email"
required
></v-text-field>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
<!-- Messages -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-list
subheader
two-line
>
<v-subheader>Messages ({{getPost.messages.length}})</v-subheader>
<template v-for="message in getPost.messages">
<v-divider :key="message._id"></v-divider>
<v-list-tile
avatar
inset
:key="message.title"
>
<v-list-tile-avatar>
<img :src="message.messageUser.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{message.messageBody}}
</v-list-tile-title>
<v-list-tile-sub-title>
{{message.messageUser.username}}
<span class="grey--text text--lighten-1 hidden-xs-only">{{message.messageDate}}</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action class='hidden-xs-only'>
<v-icon :color="checkIfOwnMessage(message) ? 'accent' : 'grey'">chat_bubble</v-icon>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-flex>
</v-layout>
</div>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
import {
GET_POST,
ADD_POST_MESSAGE,
LIKE_POST,
UNLIKE_POST
} from "../../queries";
export default {
name: "Post",
props: ["postId"],
data() {
return {
postLiked: false,
dialog: false,
messageBody: "",
isFormValid: true,
messageRules: [
message => !!message || "Message is required",
message =>
message && message.length < 75 || "Message must be less than 75 characters"
]
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user", "userFavorites"])
},
methods: {
checkIfPostLiked(postId) {
// check if user favorites includes post with id of 'postId'
this.postLiked = this.userFavorites &&
this.userFavorites.some(fave => fave._id === postId)
return this.postLiked
},
handleToggleLike() {
if (this.postLiked) {
this.handleUnlikePost();
} else {
this.handleLikePost();
}
},
handleLikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: LIKE_POST,
variables,
update: (cache, { data: { likePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes += 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.likePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleUnlikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: UNLIKE_POST,
variables,
update: (cache, { data: { unlikePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes -= 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.unlikePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleAddPostMessage() {
if (this.$refs.form.validate()) {
const variables = {
messageBody: this.messageBody,
userId: this.user._id,
postId: this.postId
};
this.$apollo
.mutate({
mutation: ADD_POST_MESSAGE,
variables,
update: (cache, { data: { addPostMessage } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.messages.unshift(addPostMessage);
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
this.$refs.form.reset();
console.log(data.addPostMessage);
})
.catch(err => console.error(err));
}
},
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
},
checkIfOwnMessage(message) {
return this.user && this.user._id === message.messageUser._id;
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We need to test if it works.



68. Add Like Notification in Profile Tab 5min
- We need to modify the
App.vueHome page to add a badge in theProfilebutton when we like any of the posts.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
:class="{ 'bounce': badgeAnimated }"
>
<span slot="badge" v-if="userFavorites.length">{{userFavorites.length}}</span>
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false,
badgeAnimated: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
},
userFavorites(value) {
// if user favorites value changed at all
if (value) {
this.badgeAnimated = true;
setTimeout(() => (this.badgeAnimated = false), 1000);
}
}
},
computed: {
...mapGetters(["authError", "user", "userFavorites"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* User Favorite Animation */
.bounce {
animation: bounce 1s both;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -20px, 0);
}
70% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
</style>
- We need to test if it works.



Section 13: Search Posts 0 / 4|22min
69. Add typeDef / resolver / query for searchPosts 7min
- We need to modify the
models\Post.jsdocument to create index to search on all fields of posts
models\Post.js
const mongoose = require("mongoose");
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
imageUrl: {
type: String,
required: true
},
categories: {
type: [String],
required: true
},
description: {
type: String,
required: true
},
createdDate: {
type: Date,
default: Date.now
},
likes: {
type: Number,
default: 0
},
// property ('createdBy') === path
// ref ('User') === model
createdBy: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User"
},
messages: [
{
messageBody: {
type: String,
required: true
},
messageDate: {
type: Date,
default: Date.now
},
messageUser: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "User"
}
}
]
});
// Create index to search on all fields of posts
PostSchema.index({
'$**': 'text'
});
module.exports = mongoose.model("Post", PostSchema);
- We need to modify the
typeDefs.gqldocument to include thesearchPostsQuery.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
# Likes for Post / Favorites for User
type LikesFaves {
likes: Int
favorites: [Post]
}
type Query {
getCurrentUser: User
getPosts: [Post]
getPost(postId: ID!): Post!
searchPosts(searchTerm: String): [Post]
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
likePost(postId: ID!, username: String!): LikesFaves!
unlikePost(postId: ID!, username: String!): LikesFaves!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We need to modify the
resolvers.jsdocument to include the thesearchPostsQuery.
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
searchPosts: async (_, { searchTerm }, { Post }) => {
if (searchTerm) {
const searchResults = await Post.find(
// Perform text search for search value of 'searchTerm'
{ $text: { $search: searchTerm } },
// Assign 'searchTerm' a text score to provide best match
{ score: { $meta: "textScore" } }
)
// Sort results according to that textScore (as well as by likes in descending order)
.sort({
score: { $meta: "textScore" },
likes: "desc"
})
.limit(5);
return searchResults;
}
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
likePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add 1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: 1 } },
{ new: true }
);
// Find User, add id of post to its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $addToSet: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
unlikePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add -1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: -1 } },
{ new: true }
);
// Find User, remove id of post from its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $pull: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We need to modify the
client\src\queries.jsdocument to include theSEARCH_POSTquery.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
export const SEARCH_POSTS = gql`
query($searchTerm: String) {
searchPosts(searchTerm: $searchTerm) {
_id
title
description
imageUrl
likes
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
export const LIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
likePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
export const UNLIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
unlikePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- Ensure there are no error when running the application.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm run dev
> fullstack-vue-graphql-starter@1.0.0 dev C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> concurrently --names "server,client" "npm run server --silent" "npm run client --silent"
[server] [nodemon] 1.19.1
[server] [nodemon] to restart at any time, enter `rs`
[server] [nodemon] watching: *.*
[server] [nodemon] starting `node server.js`
[client] INFO Starting development server...
[server] Server listening on http://localhost:4000/
[server] DB connected
[client] 98% after emitting CopyPlugin DONE Compiled successfully in 51581ms6:27:18 AM
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.1.64:8080/
[client]
[client] Note that the development build is not optimized.
[client] To create a production build, run npm run build.
[client]
70. Fire searchPosts Action, Log Search Results 4min
- We need to modify the
client\src\store.jsstore document to include thesearchPostsaction
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
SEARCH_POSTS,
ADD_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
}
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
searchPosts: ( _, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
console.log(data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to modify the
src\App.vueHome document to allow to search results.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
v-model="searchTerm"
@input="handleSearchPosts"
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
:class="{ 'bounce': badgeAnimated }"
>
<span slot="badge" v-if="userFavorites.length">{{userFavorites.length}}</span>
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
searchTerm: "",
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false,
badgeAnimated: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
},
userFavorites(value) {
// if user favorites value changed at all
if (value) {
this.badgeAnimated = true;
setTimeout(() => (this.badgeAnimated = false), 1000);
}
}
},
computed: {
...mapGetters(["authError", "user", "userFavorites"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSearchPosts() {
this.$store.dispatch("searchPosts", {
searchTerm: this.searchTerm
});
},
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* User Favorite Animation */
.bounce {
animation: bounce 1s both;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -20px, 0);
}
70% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
</style>
- We need to test if it works.



71. Add searchResults to State, Build Search Result Card 5min
- We need to modify the
client\src\store.jsstore document to include thesearchResultsstate.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
SEARCH_POSTS,
ADD_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
searchResults: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setSearchResults: (state, payload) => {
if (payload !== null) {
state.searchResults = payload;
}
},
setUser: (state, payload) => {
state.user = payload
},
setLoading: (state, payload) => {
state.loading = payload
},
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
clearSearchResults: state => (state.searchResults = []),
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
searchPosts: ( { commit }, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setSearchResults", data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
searchResults: state => state.searchResults,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to modify the
src\App.vueHome document to make us of thesearchResultsstate.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
v-model="searchTerm"
@input="handleSearchPosts"
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<!-- Search Results Card -->
<v-card
dark
v-if="searchResults.length"
id="search__card"
>
<v-list>
<v-list-tile
v-for="result in searchResults"
:key="result._id"
>
<v-list-tile-title>
{{result.title}} -
<span class="font-weight-thin">{{result.description}}</span>
</v-list-tile-title>
</v-list-tile>
</v-list>
</v-card>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
:class="{ 'bounce': badgeAnimated }"
>
<span
slot="badge"
v-if="userFavorites.length"
>{{userFavorites.length}}</span>
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
searchTerm: "",
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false,
badgeAnimated: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
},
userFavorites(value) {
// if user favorites value changed at all
if (value) {
this.badgeAnimated = true;
setTimeout(() => (this.badgeAnimated = false), 1000);
}
}
},
computed: {
...mapGetters(["searchResults", "authError", "user", "userFavorites"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSearchPosts() {
this.$store.dispatch("searchPosts", {
searchTerm: this.searchTerm
});
},
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* Search Results Card */
#search__card {
position: absolute;
width: 100vw;
z-index: 8;
top: 100%;
left: 0%;
}
/* User Favorite Animation */
.bounce {
animation: bounce 1s both;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -20px, 0);
}
70% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
</style>
- We need to test if it works.

72. Finishing Search Results, Making them Functional 6min
- We need to modify the
src\App.vueHome document to be able to access any of the search post results.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
v-model="searchTerm"
@input="handleSearchPosts"
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<!-- Search Results Card -->
<v-card
dark
v-if="searchResults.length"
id="search__card"
>
<v-list>
<v-list-tile
v-for="result in searchResults"
:key="result._id"
@click="goToSearchResult(result._id)"
>
<v-list-tile-title>
{{result.title}} -
<span class="font-weight-thin">{{formatDescription(result.description)}}</span>
</v-list-tile-title>
<!-- Show Icon if Result Favorited by User -->
<v-list-tile-action v-if="checkIfUserFavorite(result._id)">
<v-icon>favorite</v-icon>
</v-list-tile-action>
</v-list-tile>
</v-list>
</v-card>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
:class="{ 'bounce': badgeAnimated }"
>
<span
slot="badge"
v-if="userFavorites.length"
>{{userFavorites.length}}</span>
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
searchTerm: "",
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false,
badgeAnimated: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
},
userFavorites(value) {
// if user favorites value changed at all
if (value) {
this.badgeAnimated = true;
setTimeout(() => (this.badgeAnimated = false), 1000);
}
}
},
computed: {
...mapGetters(["searchResults", "authError", "user", "userFavorites"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSearchPosts() {
this.$store.dispatch("searchPosts", {
searchTerm: this.searchTerm
});
},
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
goToSearchResult(resultId) {
// Clear search term
this.searchTerm = "";
// Go to desired result
this.$router.push(`/posts/${resultId}`);
// Clear search results
this.$store.commit("clearSearchResults");
},
formatDescription(desc) {
return desc.length > 30 ? `${desc.slice(0, 30)}...` : desc;
},
checkIfUserFavorite(resultId) {
return (
this.userFavorites &&
this.userFavorites.some(fave => fave._id === resultId)
);
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* Search Results Card */
#search__card {
position: absolute;
width: 100vw;
z-index: 8;
top: 100%;
left: 0%;
}
/* User Favorite Animation */
.bounce {
animation: bounce 1s both;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -20px, 0);
}
70% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
</style>
- We need to test if it works.




Section 14: Profile Page, Update / Delete Posts 0 / 7|53min
73. Add User Details Card / Favorites Cards 6min
- We are going to modify the 'Auth\Profile.vue' page document to a User details card and Favorites Post cards for the User.
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-card-media
height="125px"
contain
:src="user.avatar"
></v-card-media>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{user.joinDate}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">2 Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">{{userFavorites.length}}</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-card-media
height="30vh"
:src="favorite.imageUrl"
></v-card-media>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Profile",
computed: {
...mapGetters(["user", "userFavorites"])
}
};
</script>
- We need to test if it works.


74. Write getUserPosts Query 4min
- We are going to modify the
typeDefs.gqldocument to include thegetUserPostsQuery
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
# Likes for Post / Favorites for User
type LikesFaves {
likes: Int
favorites: [Post]
}
type Query {
getCurrentUser: User
getPosts: [Post]
getUserPosts(userId: ID!): [Post]
getPost(postId: ID!): Post!
searchPosts(searchTerm: String): [Post]
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
likePost(postId: ID!, username: String!): LikesFaves!
unlikePost(postId: ID!, username: String!): LikesFaves!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We are going to modify the
resolvers.jsdocument to include thegetUserPostsQuery
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getUserPosts: async (_, { userId }, { Post }) => {
const posts = await Post.find({
createdBy: userId
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
searchPosts: async (_, { searchTerm }, { Post }) => {
if (searchTerm) {
const searchResults = await Post.find(
// Perform text search for search value of 'searchTerm'
{ $text: { $search: searchTerm } },
// Assign 'searchTerm' a text score to provide best match
{ score: { $meta: "textScore" } }
)
// Sort results according to that textScore (as well as by likes in descending order)
.sort({
score: { $meta: "textScore" },
likes: "desc"
})
.limit(5);
return searchResults;
}
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
likePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add 1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: 1 } },
{ new: true }
);
// Find User, add id of post to its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $addToSet: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
unlikePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add -1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: -1 } },
{ new: true }
);
// Find User, remove id of post from its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $pull: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We are going to modify the
client\src\queries.jsto add theGET_USER_POSTSquery.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
export const SEARCH_POSTS = gql`
query($searchTerm: String) {
searchPosts(searchTerm: $searchTerm) {
_id
title
description
imageUrl
likes
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const GET_USER_POSTS = gql`
query($userId: ID!) {
getUserPosts(userId: $userId) {
_id
title
imageUrl
description
categories
createdDate
likes
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
export const LIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
likePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
export const UNLIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
unlikePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We need to ensure there are no errors
[server] [nodemon] restarting due to changes...
[client] WAIT Compiling...5:46:10 AM
[client]
[client] 10% building 0/0 modules 0 active[nodemon] starting `node server.js`
98% after emitting DONE Compiled successfully in 1046ms5:46:12 AM
[client]
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
- Network: http://192.168.1.38:8080/
[client]
[server] Server listening on http://localhost:4000/
[server] DB connected
75. Execute getUserPosts Query, Create and Populate User Cards 9min
- We are going to modify the
store.jsstore document to include theuserPostsstate.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
GET_USER_POSTS,
SEARCH_POSTS,
ADD_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
userPosts: [],
searchResults: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setSearchResults: (state, payload) => {
if (payload !== null) {
state.searchResults = payload;
}
},
setUser: (state, payload) => {
state.user = payload
},
setUserPosts: (state, payload) => {
state.userPosts = payload;
},
setLoading: (state, payload) => {
state.loading = payload
},
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
clearSearchResults: state => (state.searchResults = []),
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getUserPosts: ({ commit }, payload) => {
apolloClient
.query({
query: GET_USER_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setUserPosts", data.getUserPosts);
// console.log(data.getUserPosts);
})
.catch(err => {
console.error(err);
});
},
searchPosts: ( { commit }, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setSearchResults", data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
userPosts: state => state.userPosts,
searchResults: state => state.searchResults,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We are going to modify the
Auth\Profile.vuepage document to include the posts created by the user
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{user.joinDate}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Profile",
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
}
}
};
</script>
- We need to test if it works.


76. Add Edit Post Dialog for Updating User Posts 6min
- We are going to modify the
Auth\Profile.vuepage document to add the edit Post dialog for updating the user posts
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{user.joinDate}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
@click="editPostDialog = true"
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Edit Post Dialog -->
<v-dialog xs12 sm6 offset-sm3 persistent v-model="editPostDialog">
<v-card>
<v-card-title class="headline grey lighten-2">Update Post</v-card-title>
<v-container>
<v-form v-model="isFormValid" lazy-validation ref="form" @submit.prevent="handleUpdateUserPost">
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="titleRules" v-model="title" label="Post Title" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="imageRules" v-model="imageUrl" label="Image URL" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img :src="imageUrl" height="300px">
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select v-model="categories" :rules="categoriesRules" :items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']" multiple label="Categories"></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea :rules="descRules" v-model="description" label="Description" type="text" required></v-textarea>
</v-flex>
</v-layout>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn type="submit" class="success--text" flat>Update</v-btn>
<v-btn class="error--text" flat @click="editPostDialog = false">Cancel</v-btn>
</v-card-actions>
</v-form>
</v-container>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Profile",
data() {
return {
editPostDialog: false,
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
},
handleUpdateUserPost() {
// update user post action
}
}
};
</script>
- We need to test if it works.



77. Create updateUserPost Mutation 8min
- We are going to modify the
typeDefs.gqldocument to include theupdateUserPostMutation.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
# Likes for Post / Favorites for User
type LikesFaves {
likes: Int
favorites: [Post]
}
type Query {
getCurrentUser: User
getPosts: [Post]
getUserPosts(userId: ID!): [Post]
getPost(postId: ID!): Post!
searchPosts(searchTerm: String): [Post]
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
updateUserPost(
postId: ID!
userId: ID!
title: String!
imageUrl: String!
categories: [String]!
description: String!
): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
likePost(postId: ID!, username: String!): LikesFaves!
unlikePost(postId: ID!, username: String!): LikesFaves!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We are going to modify the
resolvers.jsdocument to include theupdateUserPostMutation.
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getUserPosts: async (_, { userId }, { Post }) => {
const posts = await Post.find({
createdBy: userId
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
searchPosts: async (_, { searchTerm }, { Post }) => {
if (searchTerm) {
const searchResults = await Post.find(
// Perform text search for search value of 'searchTerm'
{ $text: { $search: searchTerm } },
// Assign 'searchTerm' a text score to provide best match
{ score: { $meta: "textScore" } }
)
// Sort results according to that textScore (as well as by likes in descending order)
.sort({
score: { $meta: "textScore" },
likes: "desc"
})
.limit(5);
return searchResults;
}
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
updateUserPost: async (
_,
{ postId, userId, title, imageUrl, categories, description },
{ Post }
) => {
const post = await Post.findOneAndUpdate(
// Find post by postId and createdBy
{ _id: postId, createdBy: userId },
{ $set: { title, imageUrl, categories, description } },
{ new: true }
);
return post;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
likePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add 1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: 1 } },
{ new: true }
);
// Find User, add id of post to its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $addToSet: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
unlikePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add -1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: -1 } },
{ new: true }
);
// Find User, remove id of post from its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $pull: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We are going to modify the
client\src\queries.jsto add theUPDATE_USER_POSTMutation.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
export const SEARCH_POSTS = gql`
query($searchTerm: String) {
searchPosts(searchTerm: $searchTerm) {
_id
title
description
imageUrl
likes
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const GET_USER_POSTS = gql`
query($userId: ID!) {
getUserPosts(userId: $userId) {
_id
title
imageUrl
description
categories
createdDate
likes
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const UPDATE_USER_POST = gql`
mutation(
$postId: ID!
$userId: ID!
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
) {
updateUserPost(
postId: $postId
userId: $userId
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
) {
_id
title
imageUrl
description
categories
createdDate
likes
createdBy {
_id
avatar
}
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
export const LIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
likePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
export const UNLIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
unlikePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We need to ensure that everything works properly.
[client] [nodemon] restarting due to changes...
[client] WAIT Compiling...6:40:35 AM
[client]
[server] [nodemon] restarting due to changes...
[server] [nodemon] restarting due to changes...
[client] 40% building 0/1 modules 1 active ...he-ultimate-guide\client\src\queries.js[nodemon] starting `node server.js`
DONE Compiled successfully in 1458ms6:40:37 AM
[client]
[client]
[client] App running at:
[client] - Local: http://localhost:8080/
[client] - Network: http://192.168.1.38:8080/
[client]
[server] Server listening on http://localhost:4000/
[server] DB connected
78. Executing updateUserPost Mutation with Vuex Action 13min
- We need to modify the
store.jsstore document to create theupdateUserPostaction.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
GET_USER_POSTS,
SEARCH_POSTS,
ADD_POST,
UPDATE_USER_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
userPosts: [],
searchResults: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setSearchResults: (state, payload) => {
if (payload !== null) {
state.searchResults = payload;
}
},
setUser: (state, payload) => {
state.user = payload
},
setUserPosts: (state, payload) => {
state.userPosts = payload;
},
setLoading: (state, payload) => {
state.loading = payload
},
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
clearSearchResults: state => (state.searchResults = []),
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getUserPosts: ({ commit }, payload) => {
apolloClient
.query({
query: GET_USER_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setUserPosts", data.getUserPosts);
// console.log(data.getUserPosts);
})
.catch(err => {
console.error(err);
});
},
searchPosts: ( { commit }, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setSearchResults", data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
updateUserPost: ({ state, commit }, payload) => {
apolloClient
.mutate({
mutation: UPDATE_USER_POST,
variables: payload
})
.then(({ data }) => {
const index = state.userPosts.findIndex(
post => post._id === data.updateUserPost._id
);
const userPosts = [
...state.userPosts.slice(0, index),
data.updateUserPost,
...state.userPosts.slice(index + 1)
];
commit("setUserPosts", userPosts);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
userPosts: state => state.userPosts,
searchResults: state => state.searchResults,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to modify the
Auth\Profile.vuepage to handle the change of a User Post.
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{user.joinDate}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
@click="loadPost(post)"
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Edit Post Dialog -->
<v-dialog
xs12
sm6
offset-sm3
persistent
v-model="editPostDialog"
>
<v-card>
<v-card-title class="headline grey lighten-2">Update Post</v-card-title>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleUpdateUserPost"
>
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="titleRules"
v-model="title"
label="Post Title"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="imageRules"
v-model="imageUrl"
label="Image URL"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img
:src="imageUrl"
height="300px"
>
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select
v-model="categories"
:rules="categoriesRules"
:items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']"
multiple
label="Categories"
></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea
:rules="descRules"
v-model="description"
label="Description"
type="text"
required
></v-textarea>
</v-flex>
</v-layout>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isFormValid"
type="submit"
class="success--text"
flat
>Update</v-btn>
<v-btn
class="error--text"
flat
@click="editPostDialog = false"
>Cancel</v-btn>
</v-card-actions>
</v-form>
</v-container>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Profile",
data() {
return {
editPostDialog: false,
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
},
handleUpdateUserPost() {
if (this.$refs.form.validate()) {
this.$store.dispatch("updateUserPost", {
postId: this.postId,
userId: this.user._id,
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description
});
this.editPostDialog = false;
}
},
loadPost(
{ _id, title, imageUrl, categories, description },
editPostDialog = true
) {
this.editPostDialog = editPostDialog;
this.postId = _id;
this.title = title;
this.imageUrl = imageUrl;
this.categories = categories;
this.description = description;
}
}
};
</script>






79. deleteUserRecipe Mutation - Backend Creation to Frontend Execution 7min
- We are going to modify the
typeDefs.gqldocument to include thedeleteUserRecipeMutation.
typeDefs.gql
type User {
_id: ID
username: String! @unique
email: String!
password: String!
avatar: String
joinDate: String
favorites: [Post]
}
type Post {
_id: ID
title: String!
imageUrl: String!
categories: [String]!
description: String!
createdDate: String
likes: Int
createdBy: User!
messages: [Message]
}
type Message {
_id: ID
messageBody: String!
messageDate: String
messageUser: User!
}
type Token {
token: String!
}
type PostsPage {
posts: [Post]
hasMore: Boolean
}
# Likes for Post / Favorites for User
type LikesFaves {
likes: Int
favorites: [Post]
}
type Query {
getCurrentUser: User
getPosts: [Post]
getUserPosts(userId: ID!): [Post]
getPost(postId: ID!): Post!
searchPosts(searchTerm: String): [Post]
infiniteScrollPosts(pageNum: Int!, pageSize: Int!): PostsPage
}
type Mutation {
addPost(
title: String!
imageUrl: String!
categories: [String]!
description: String!
creatorId: ID!
): Post!
updateUserPost(
postId: ID!
userId: ID!
title: String!
imageUrl: String!
categories: [String]!
description: String!
): Post!
deleteUserPost(postId: ID!): Post!
addPostMessage(messageBody: String!, userId: ID!, postId: ID!): Message!
likePost(postId: ID!, username: String!): LikesFaves!
unlikePost(postId: ID!, username: String!): LikesFaves!
signinUser(username: String!, password: String!): Token
signupUser(username: String!, email: String!, password: String!): Token
}
- We are going to modify the
resolvers.jsdocument to include thedeleteUserRecipeMutation.
resolvers.js
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const createToken = (user, secret, expiresIn) => {
const { username, email } = user;
return jwt.sign({ username, email }, secret, { expiresIn });
};
module.exports = {
Query: {
getCurrentUser: async (_, _args, { User, currentUser }) => {
if (!currentUser) {
return null;
}
const user = await User.findOne({
username: currentUser.username
}).populate({
path: "favorites",
model: "Post"
});
return user;
},
getPosts: async (_, _args, { Post }) => {
const posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
});
return posts;
},
getUserPosts: async (_, { userId }, { Post }) => {
const posts = await Post.find({
createdBy: userId
});
return posts;
},
getPost: async (_, { postId }, { Post }) => {
const post = await Post.findOne({ _id: postId }).populate({
path: "messages.messageUser",
model: "User"
});
return post;
},
searchPosts: async (_, { searchTerm }, { Post }) => {
if (searchTerm) {
const searchResults = await Post.find(
// Perform text search for search value of 'searchTerm'
{ $text: { $search: searchTerm } },
// Assign 'searchTerm' a text score to provide best match
{ score: { $meta: "textScore" } }
)
// Sort results according to that textScore (as well as by likes in descending order)
.sort({
score: { $meta: "textScore" },
likes: "desc"
})
.limit(5);
return searchResults;
}
},
infiniteScrollPosts: async (_, { pageNum, pageSize }, { Post }) => {
let posts;
if (pageNum === 1) {
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.limit(pageSize);
} else {
// If page number is greater than one, figure out how many documents to skip
const skips = pageSize * (pageNum - 1);
posts = await Post.find({})
.sort({ createdDate: "desc" })
.populate({
path: "createdBy",
model: "User"
})
.skip(skips)
.limit(pageSize);
}
const totalDocs = await Post.countDocuments();
const hasMore = totalDocs > pageSize * pageNum;
return { posts, hasMore };
}
},
Mutation: {
addPost: async (
_,
{ title, imageUrl, categories, description, creatorId },
{ Post }
) => {
const newPost = await new Post({
title,
imageUrl,
categories,
description,
createdBy: creatorId
}).save();
return newPost;
},
updateUserPost: async (
_,
{ postId, userId, title, imageUrl, categories, description },
{ Post }
) => {
const post = await Post.findOneAndUpdate(
// Find post by postId and createdBy
{ _id: postId, createdBy: userId },
{ $set: { title, imageUrl, categories, description } },
{ new: true }
);
return post;
},
deleteUserPost: async (_, { postId }, { Post }) => {
const post = await Post.findOneAndRemove({ _id: postId });
return post;
},
addPostMessage: async (_, { messageBody, userId, postId }, { Post }) => {
const newMessage = {
messageBody,
messageUser: userId
};
const post = await Post.findOneAndUpdate(
// find post by id
{ _id: postId },
// prepend (push) new message to beginning of messages array
{ $push: { messages: { $each: [newMessage], $position: 0 } } },
// return fresh document after update
{ new: true }
).populate({
path: "messages.messageUser",
model: "User"
});
return post.messages[0];
},
likePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add 1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: 1 } },
{ new: true }
);
// Find User, add id of post to its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $addToSet: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
unlikePost: async (_, { postId, username }, { Post, User }) => {
// Find Post, add -1 to its 'like' value
const post = await Post.findOneAndUpdate(
{ _id: postId },
{ $inc: { likes: -1 } },
{ new: true }
);
// Find User, remove id of post from its favorites array (which will be populated as Posts)
const user = await User.findOneAndUpdate(
{ username },
{ $pull: { favorites: postId } },
{ new: true }
).populate({
path: "favorites",
model: "Post"
});
// Return only likes from 'post' and favorites from 'user'
return { likes: post.likes, favorites: user.favorites };
},
signinUser: async (_, { username, password }, { User }) => {
const user = await User.findOne({ username });
if (!user) {
throw new Error("User not found");
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error("Invalid password");
}
return { token: createToken(user, process.env.SECRET, "1hr") };
},
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return { token: createToken(newUser, process.env.SECRET, "1hr") };
}
}
};
- We are going to modify the
client\src\queries.jsto add theDELETE_USER_POSTMutation.
client\src\queries.js
import { gql } from "apollo-boost";
/* Posts Queries */
export const GET_POSTS = gql`
query {
getPosts {
_id
title
imageUrl
}
}
`;
export const GET_POST = gql`
query($postId: ID!) {
getPost(postId: $postId) {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
}
`;
export const SEARCH_POSTS = gql`
query($searchTerm: String) {
searchPosts(searchTerm: $searchTerm) {
_id
title
description
imageUrl
likes
}
}
`;
/* User Queries */
export const GET_CURRENT_USER = gql`
query {
getCurrentUser {
_id
username
email
password
avatar
joinDate
favorites {
_id
title
imageUrl
}
}
}
`;
export const GET_USER_POSTS = gql`
query($userId: ID!) {
getUserPosts(userId: $userId) {
_id
title
imageUrl
description
categories
createdDate
likes
}
}
`;
export const INFINITE_SCROLL_POSTS = gql`
query($pageNum: Int!, $pageSize: Int!) {
infiniteScrollPosts(pageNum: $pageNum, pageSize: $pageSize) {
hasMore
posts {
_id
title
imageUrl
categories
description
likes
createdDate
messages {
_id
}
createdBy {
_id
username
avatar
}
}
}
}
`;
/* Posts Mutations */
export const ADD_POST = gql`
mutation(
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
$creatorId: ID!
) {
addPost(
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
creatorId: $creatorId
) {
_id
title
imageUrl
categories
description
}
}
`;
export const UPDATE_USER_POST = gql`
mutation(
$postId: ID!
$userId: ID!
$title: String!
$imageUrl: String!
$categories: [String]!
$description: String!
) {
updateUserPost(
postId: $postId
userId: $userId
title: $title
imageUrl: $imageUrl
categories: $categories
description: $description
) {
_id
title
imageUrl
description
categories
createdDate
likes
createdBy {
_id
avatar
}
}
}
`;
export const DELETE_USER_POST = gql`
mutation($postId: ID!) {
deleteUserPost(postId: $postId) {
_id
}
}
`;
export const ADD_POST_MESSAGE = gql`
mutation($messageBody: String!, $userId: ID!, $postId: ID!) {
addPostMessage(
messageBody: $messageBody
userId: $userId
postId: $postId
) {
_id
messageBody
messageDate
messageUser {
_id
username
avatar
}
}
}
`;
export const LIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
likePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
export const UNLIKE_POST = gql`
mutation($postId: ID!, $username: String!) {
unlikePost(postId: $postId, username: $username) {
likes
favorites {
_id
title
imageUrl
}
}
}
`;
/* User Mutations */
export const SIGNIN_USER = gql`
mutation($username: String!, $password: String!) {
signinUser(username: $username, password: $password) {
token
}
}
`;
export const SIGNUP_USER = gql`
mutation($username: String!, $email: String!, $password: String!) {
signupUser(username: $username, email: $email, password: $password) {
token
}
}
`;
- We need to modify the
store.jsstore document to create thedeleteUserPostaction.
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
GET_USER_POSTS,
SEARCH_POSTS,
ADD_POST,
UPDATE_USER_POST,
DELETE_USER_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
userPosts: [],
searchResults: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setSearchResults: (state, payload) => {
if (payload !== null) {
state.searchResults = payload;
}
},
setUser: (state, payload) => {
state.user = payload
},
setUserPosts: (state, payload) => {
state.userPosts = payload;
},
setLoading: (state, payload) => {
state.loading = payload
},
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
clearSearchResults: state => (state.searchResults = []),
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getUserPosts: ({ commit }, payload) => {
apolloClient
.query({
query: GET_USER_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setUserPosts", data.getUserPosts);
// console.log(data.getUserPosts);
})
.catch(err => {
console.error(err);
});
},
searchPosts: ( { commit }, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setSearchResults", data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
}
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
updateUserPost: ({ state, commit }, payload) => {
apolloClient
.mutate({
mutation: UPDATE_USER_POST,
variables: payload
})
.then(({ data }) => {
const index = state.userPosts.findIndex(
post => post._id === data.updateUserPost._id
);
const userPosts = [
...state.userPosts.slice(0, index),
data.updateUserPost,
...state.userPosts.slice(index + 1)
];
commit("setUserPosts", userPosts);
})
.catch(err => {
console.error(err);
});
},
deleteUserPost: ({ state, commit }, payload) => {
apolloClient
.mutate({
mutation: DELETE_USER_POST,
variables: payload
})
.then(({ data }) => {
const index = state.userPosts.findIndex(
post => post._id === data.deleteUserPost._id
);
const userPosts = [
...state.userPosts.slice(0, index),
...state.userPosts.slice(index + 1)
];
commit("setUserPosts", userPosts);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
userPosts: state => state.userPosts,
searchResults: state => state.searchResults,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We are going to modify the
Auth\Profile.vuepage document to delete a User Post.
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{user.joinDate}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
@click="loadPost(post)"
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
@click="handleDeleteUserPost(post)"
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Edit Post Dialog -->
<v-dialog
xs12
sm6
offset-sm3
persistent
v-model="editPostDialog"
>
<v-card>
<v-card-title class="headline grey lighten-2">Update Post</v-card-title>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleUpdateUserPost"
>
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="titleRules"
v-model="title"
label="Post Title"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="imageRules"
v-model="imageUrl"
label="Image URL"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img
:src="imageUrl"
height="300px"
>
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select
v-model="categories"
:rules="categoriesRules"
:items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']"
multiple
label="Categories"
></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea
:rules="descRules"
v-model="description"
label="Description"
type="text"
required
></v-textarea>
</v-flex>
</v-layout>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isFormValid"
type="submit"
class="success--text"
flat
>Update</v-btn>
<v-btn
class="error--text"
flat
@click="editPostDialog = false"
>Cancel</v-btn>
</v-card-actions>
</v-form>
</v-container>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Profile",
data() {
return {
editPostDialog: false,
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
},
handleUpdateUserPost() {
if (this.$refs.form.validate()) {
this.$store.dispatch("updateUserPost", {
postId: this.postId,
userId: this.user._id,
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description
});
this.editPostDialog = false;
}
},
handleDeleteUserPost(post) {
this.loadPost(post, false);
const deletePost = window.confirm(
"Are you sure you want to delete this post?"
);
if (deletePost) {
this.$store.dispatch("deleteUserPost", {
postId: this.postId
});
}
},
loadPost(
{ _id, title, imageUrl, categories, description },
editPostDialog = true
) {
this.editPostDialog = editPostDialog;
this.postId = _id;
this.title = title;
this.imageUrl = imageUrl;
this.categories = categories;
this.description = description;
}
}
};
</script>



- Modify the
server.jsdocument to amend the "DeprecationWarning: Mongoose:findOneAndUpdate()andfindOneAndDelete()without theuseFindAndModifyoption set to false are deprecated." message
server.js
const { ApolloServer, AuthenticationError } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Verify JWT Token passed from client
const getUser = async token => {
if (token) {
try {
return await jwt.verify(token, process.env.SECRET);
} catch (err) {
throw new AuthenticationError(
"Your session has ended. Please sign in again."
);
}
}
};
// Create Apollo/GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: error => ({
name: error.name,
message: error.message.replace("Context creation failed:", "")
}),
context: async ({ req }) => {
const token = req.headers["authorization"];
return { User, Post, currentUser: await getUser(token) };
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
Section 15: Preparing for Deployment 0 / 3|17min
80. RefetchQueries for Fresh Data upon Executing Mutations 4min
- We need to modify the
store.jsstore document to add RefetchQueries for Fresh Data upon Executing Mutations
client\src\store.js
import Vue from "vue"
import Vuex from "vuex"
import router from "./router"
import { defaultClient as apolloClient } from "./main"
import {
GET_CURRENT_USER,
GET_POSTS,
GET_USER_POSTS,
INFINITE_SCROLL_POSTS,
SEARCH_POSTS,
ADD_POST,
UPDATE_USER_POST,
DELETE_USER_POST,
SIGNIN_USER,
SIGNUP_USER
} from "./queries";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
posts: [],
userPosts: [],
searchResults: [],
user: null,
loading: false,
error: null,
authError: null
},
mutations: {
setPosts: (state, payload) => {
state.posts = payload
},
setSearchResults: (state, payload) => {
if (payload !== null) {
state.searchResults = payload;
}
},
setUser: (state, payload) => {
state.user = payload
},
setUserPosts: (state, payload) => {
state.userPosts = payload;
},
setLoading: (state, payload) => {
state.loading = payload
},
setError: (state, payload) => {
state.error = payload
},
setAuthError: (state, payload) => {
state.authError = payload
},
clearUser: state => (state.user = null),
clearError: state => (state.error = null),
clearSearchResults: state => (state.searchResults = []),
},
actions: {
getCurrentUser: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_CURRENT_USER
})
.then(({ data }) => {
commit("setLoading", false)
// Add user data to state
commit("setUser", data.getCurrentUser)
console.log(data.getCurrentUser)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getPosts: ({ commit }) => {
commit("setLoading", true)
apolloClient
.query({
query: GET_POSTS
})
.then(({ data }) => {
commit("setPosts", data.getPosts)
commit("setLoading", false)
})
.catch(err => {
commit("setLoading", false)
console.error(err)
})
},
getUserPosts: ({ commit }, payload) => {
apolloClient
.query({
query: GET_USER_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setUserPosts", data.getUserPosts);
// console.log(data.getUserPosts);
})
.catch(err => {
console.error(err);
});
},
searchPosts: ( { commit }, payload) => {
apolloClient
.query({
query: SEARCH_POSTS,
variables: payload
})
.then(({ data }) => {
commit("setSearchResults", data.searchPosts);
})
.catch(err => console.error(err));
},
signinUser: ({commit}, payload) => {
commit("clearError")
commit("setLoading", true)
apolloClient
.mutate({
mutation: SIGNIN_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false)
localStorage.setItem("token", data.signinUser.token)
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go()
})
.catch(err => {
commit("setLoading", false)
commit("setError", err)
console.error(err)
})
},
addPost: (_ , payload) => {
console.log(`payload: `,payload)
apolloClient
.mutate({
mutation: ADD_POST,
variables: payload,
update: (cache, { data: { addPost } }) => {
// First read the query you want to update
const data = cache.readQuery({ query: GET_POSTS });
if (data.getPosts) {
// Create updated data
data.getPosts.unshift(addPost);
// Write updated data back to query
console.log(data);
cache.writeQuery({
query: GET_POSTS,
data
});
}
},
// optimistic response ensures data is added immediately as we specified for the update function
optimisticResponse: {
__typename: 'Mutation',
addPost: {
__typename: 'Post',
_id: -1,
...payload
}
},
// Rerun specified queries after performing the mutation in order to get fresh data
refetchQueries: [
{
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize: 2
}
}
]
})
.then(({ data }) => {
console.log(data.addPost);
})
.catch(err => {
console.error(err);
});
},
updateUserPost: ({ state, commit }, payload) => {
apolloClient
.mutate({
mutation: UPDATE_USER_POST,
variables: payload
})
.then(({ data }) => {
const index = state.userPosts.findIndex(
post => post._id === data.updateUserPost._id
);
const userPosts = [
...state.userPosts.slice(0, index),
data.updateUserPost,
...state.userPosts.slice(index + 1)
];
commit("setUserPosts", userPosts);
})
.catch(err => {
console.error(err);
});
},
deleteUserPost: ({ state, commit }, payload) => {
apolloClient
.mutate({
mutation: DELETE_USER_POST,
variables: payload
})
.then(({ data }) => {
const index = state.userPosts.findIndex(
post => post._id === data.deleteUserPost._id
);
const userPosts = [
...state.userPosts.slice(0, index),
...state.userPosts.slice(index + 1)
];
commit("setUserPosts", userPosts);
})
.catch(err => {
console.error(err);
});
},
signupUser: ({ commit }, payload) => {
commit("clearError");
commit("setLoading", true);
apolloClient
.mutate({
mutation: SIGNUP_USER,
variables: payload
})
.then(({ data }) => {
commit("setLoading", false);
localStorage.setItem("token", data.signupUser.token);
// to make sure created method is run in main.js (we run getCurrentUser), reload the page
router.go();
})
.catch(err => {
commit("setLoading", false);
commit("setError", err);
console.error(err);
});
},
signoutUser: async ({ commit }) => {
// clear user in state
commit("clearUser")
// remove token in localStorage
localStorage.setItem("token", "")
// end session
await apolloClient.resetStore()
// redirect home - kick users out of private pages (i.e. profile)
router.push("/")
}
},
getters: {
posts: state => state.posts,
userPosts: state => state.userPosts,
searchResults: state => state.searchResults,
user: state => state.user,
userFavorites: state => state.user && state.user.favorites,
loading: state => state.loading,
error: state => state.error,
authError: state => state.authError
}
})
- We need to test if it works.





81. Formatting Dates with moment 6min
- We need to install the moment library that will be use to forrmat dates
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (master)
$ npm i moment
npm WARN apollo-boost@0.4.3 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-cache-inmemory@1.6.2 requires a peer of graphql@0.11.7 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-client@2.6.3 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link@1.2.12 requires a peer of graphql@^0.11.3 || ^0.12.3 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link-http@1.5.15 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-link-http-common@0.2.14 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN apollo-utilities@1.3.2 requires a peer of graphql@^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN graphql-tag@2.10.1 requires a peer of graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ moment@2.24.0
added 1 package from 6 contributors and audited 18451 packages in 24.14s
found 0 vulnerabilities
- We need to modify the
Auth\Profile.vuePorofile page document to usemomentto forrmat dates
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{formatJoinDate(user.joinDate)}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
@click="loadPost(post)"
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
@click="handleDeleteUserPost(post)"
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Edit Post Dialog -->
<v-dialog
xs12
sm6
offset-sm3
persistent
v-model="editPostDialog"
>
<v-card>
<v-card-title class="headline grey lighten-2">Update Post</v-card-title>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleUpdateUserPost"
>
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="titleRules"
v-model="title"
label="Post Title"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="imageRules"
v-model="imageUrl"
label="Image URL"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img
:src="imageUrl"
height="300px"
>
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select
v-model="categories"
:rules="categoriesRules"
:items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']"
multiple
label="Categories"
></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea
:rules="descRules"
v-model="description"
label="Description"
type="text"
required
></v-textarea>
</v-flex>
</v-layout>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isFormValid"
type="submit"
class="success--text"
flat
>Update</v-btn>
<v-btn
class="error--text"
flat
@click="editPostDialog = false"
>Cancel</v-btn>
</v-card-actions>
</v-form>
</v-container>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import moment from "moment";
import { mapGetters } from "vuex";
export default {
name: "Profile",
data() {
return {
editPostDialog: false,
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
formatJoinDate(date) {
return moment(new Date(date)).format("ll");
},
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
},
handleUpdateUserPost() {
if (this.$refs.form.validate()) {
this.$store.dispatch("updateUserPost", {
postId: this.postId,
userId: this.user._id,
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description
});
this.editPostDialog = false;
}
},
handleDeleteUserPost(post) {
this.loadPost(post, false);
const deletePost = window.confirm(
"Are you sure you want to delete this post?"
);
if (deletePost) {
this.$store.dispatch("deleteUserPost", {
postId: this.postId
});
}
},
loadPost(
{ _id, title, imageUrl, categories, description },
editPostDialog = true
) {
this.editPostDialog = editPostDialog;
this.postId = _id;
this.title = title;
this.imageUrl = imageUrl;
this.categories = categories;
this.description = description;
}
}
};
</script>
- We need to modify the
Posts\Post.vuepost page document to usemomentto forrmat dates
client\src\components\Posts\Post.vue
<template>
<v-container
v-if="getPost"
class="mt-3"
flexbox
center
>
<!-- Post Card -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-card hover>
<v-card-title>
<h1>{{getPost.title}}</h1>
<v-btn
@click="handleToggleLike"
large
icon
v-if="user"
>
<v-icon
large
:color="checkIfPostLiked(getPost._id) ? 'red' : 'grey'"
>favorite</v-icon>
</v-btn>
<h3 class="ml-3 font-weight-thin">{{getPost.likes}} LIKES</h3>
<v-spacer></v-spacer>
<v-icon
@click="goToPreviousPage"
color="info"
large
>arrow_back</v-icon>
</v-card-title>
<v-tooltip right>
<span>Click to enlarge image</span>
<v-img
@click="toggleImageDialog"
slot="activator"
:src="getPost.imageUrl"
id="post__image"
></v-img>
</v-tooltip>
<!-- Post Image Dialog -->
<v-dialog v-model="dialog">
<v-card>
<v-img
:src="getPost.imageUrl"
height="80vh"
></v-img>
</v-card>
</v-dialog>
<v-card-text>
<span
v-for="(category, index) in getPost.categories"
:key="index"
>
<v-chip
class="mb-3"
color="accent"
text-color="white"
>{{category}}</v-chip>
</span>
<h3>{{getPost.description}}</h3>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
<!-- Messages Section -->
<div class="mt-3">
<!-- Message Input -->
<v-layout
class="mb-3"
v-if="user"
>
<v-flex xs12>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleAddPostMessage"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="messageRules"
v-model="messageBody"
clearable
:append-outer-icon="messageBody && 'send'"
label="Add Message"
type="text"
@click:append-outer="handleAddPostMessage"
prepend-icon="email"
required
></v-text-field>
</v-flex>
</v-layout>
</v-form>
</v-flex>
</v-layout>
<!-- Messages -->
<v-layout
row
wrap
>
<v-flex xs12>
<v-list
subheader
two-line
>
<v-subheader>Messages ({{getPost.messages.length}})</v-subheader>
<template v-for="message in getPost.messages">
<v-divider :key="message._id"></v-divider>
<v-list-tile
avatar
inset
:key="message.title"
>
<v-list-tile-avatar>
<img :src="message.messageUser.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
{{message.messageBody}}
</v-list-tile-title>
<v-list-tile-sub-title>
{{message.messageUser.username}}
<span class="grey--text text--lighten-1 hidden-xs-only">{{getTimeFromNow(message.messageDate)}}</span>
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action class='hidden-xs-only'>
<v-icon :color="checkIfOwnMessage(message) ? 'accent' : 'grey'">chat_bubble</v-icon>
</v-list-tile-action>
</v-list-tile>
</template>
</v-list>
</v-flex>
</v-layout>
</div>
</v-container>
</template>
<script>
import moment from "moment";
import { mapGetters } from "vuex";
import {
GET_POST,
ADD_POST_MESSAGE,
LIKE_POST,
UNLIKE_POST
} from "../../queries";
export default {
name: "Post",
props: ["postId"],
data() {
return {
postLiked: false,
dialog: false,
messageBody: "",
isFormValid: true,
messageRules: [
message => !!message || "Message is required",
message =>
message && message.length < 75 || "Message must be less than 75 characters"
]
};
},
apollo: {
getPost: {
query: GET_POST,
variables() {
return {
postId: this.postId
};
}
}
},
computed: {
...mapGetters(["user", "userFavorites"])
},
methods: {
getTimeFromNow(time) {
return moment(new Date(time)).fromNow();
},
checkIfPostLiked(postId) {
// check if user favorites includes post with id of 'postId'
this.postLiked = this.userFavorites &&
this.userFavorites.some(fave => fave._id === postId)
return this.postLiked
},
handleToggleLike() {
if (this.postLiked) {
this.handleUnlikePost();
} else {
this.handleLikePost();
}
},
handleLikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: LIKE_POST,
variables,
update: (cache, { data: { likePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes += 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.likePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleUnlikePost() {
const variables = {
postId: this.postId,
username: this.user.username
};
this.$apollo
.mutate({
mutation: UNLIKE_POST,
variables,
update: (cache, { data: { unlikePost } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.likes -= 1;
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
const updatedUser = {
...this.user,
favorites: data.unlikePost.favorites
};
this.$store.commit("setUser", updatedUser);
})
.catch(err => console.error(err));
},
handleAddPostMessage() {
if (this.$refs.form.validate()) {
const variables = {
messageBody: this.messageBody,
userId: this.user._id,
postId: this.postId
};
this.$apollo
.mutate({
mutation: ADD_POST_MESSAGE,
variables,
update: (cache, { data: { addPostMessage } }) => {
const data = cache.readQuery({
query: GET_POST,
variables: { postId: this.postId }
});
data.getPost.messages.unshift(addPostMessage);
cache.writeQuery({
query: GET_POST,
variables: { postId: this.postId },
data
});
}
})
.then(({ data }) => {
this.$refs.form.reset();
console.log(data.addPostMessage);
})
.catch(err => console.error(err));
}
},
goToPreviousPage() {
this.$router.go(-1);
},
toggleImageDialog() {
if (window.innerWidth > 500) {
this.dialog = !this.dialog;
}
},
checkIfOwnMessage(message) {
return this.user && this.user._id === message.messageUser._id;
}
}
};
</script>
<style scoped>
#post__image {
height: 400px !important;
}
</style>
- We need to modify the
Posts\Posts.vueposts page document to usemomentto forrmat dates
client\src\components\Posts\Posts.vue
<template>
<v-container fluid grid-list-xl>
<!-- Post Cards -->
<v-layout row wrap v-if="infiniteScrollPosts">
<v-flex xs12 sm6 v-for="post in infiniteScrollPosts.posts" :key="post._id">
<v-card hover>
<v-img @click.native="goToPost(post._id)" :src="post.imageUrl" height="30vh" lazy></v-img>
<v-card-actions>
<v-card-title primary>
<div>
<div class="headline">{{post.title}}</div>
<span class="grey--text">{{post.likes}} likes - {{post.messages.length}} comments</span>
</div>
</v-card-title>
<v-spacer></v-spacer>
<v-btn @click="showPostCreator = !showPostCreator" icon>
<v-icon>{{`keyboard_arrow_${showPostCreator ? 'up' : 'down'}`}}</v-icon>
</v-btn>
</v-card-actions>
<!-- Post Creator Tile -->
<v-slide-y-transition>
<v-card-text v-show="showPostCreator" class="grey lighten-4">
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="post.createdBy.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="text--primary">{{post.createdBy.username}}</v-list-tile-title>
<v-list-tile-sub-title class="font-weight-thin">Added {{formatCreatedDate(post.createdDate)}}</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon ripple>
<v-icon color="grey lighten-1">info</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-card-text>
</v-slide-y-transition>
</v-card>
</v-flex>
</v-layout>
<!-- Fetch More Button -->
<v-layout v-if="showMoreEnabled" column>
<v-flex xs12>
<v-layout justify-center row>
<v-btn color="info" @click="showMorePosts">Fetch More</v-btn>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import moment from "moment";
import { INFINITE_SCROLL_POSTS } from "../../queries";
const pageSize = 2;
export default {
name: "Posts",
data() {
return {
pageNum: 1,
showMoreEnabled: true,
showPostCreator: false
};
},
apollo: {
infiniteScrollPosts: {
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize
}
}
},
methods: {
formatCreatedDate(date) {
return moment(new Date(date)).format("ll");
},
showMorePosts() {
this.pageNum += 1;
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log("previous result", prevResult.infiniteScrollPosts.posts);
console.log("fetch more result", fetchMoreResult);
const newPosts = fetchMoreResult.infiniteScrollPosts.posts;
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore;
this.showMoreEnabled = hasMore;
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
};
}
});
},
goToPost(postId) {
this.$router.push(`/posts/${postId}`);
}
}
};
</script>
- We need to test if it works.



82. Minor Improvements/Fixes 7min
- We are going to modify the
App.vuedocument to change theh1andh2CSS segments.
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer
app
temporary
fixed
v-model="sideNav"
>
<v-toolbar
color="accent"
dark
flat
>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile
ripple
v-for="item in sideNavItems"
:key="item.title"
:to="item.link"
>
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
<!-- Signout Button -->
<v-list-tile
v-if="user"
@click="handleSignoutUser"
>
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Signout</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
v-model="searchTerm"
@input="handleSearchPosts"
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<!-- Search Results Card -->
<v-card
dark
v-if="searchResults.length"
id="search__card"
>
<v-list>
<v-list-tile
v-for="result in searchResults"
:key="result._id"
@click="goToSearchResult(result._id)"
>
<v-list-tile-title>
{{result.title}} -
<span class="font-weight-thin">{{formatDescription(result.description)}}</span>
</v-list-tile-title>
<!-- Show Icon if Result Favorited by User -->
<v-list-tile-action v-if="checkIfUserFavorite(result._id)">
<v-icon>favorite</v-icon>
</v-list-tile-action>
</v-list-tile>
</v-list>
</v-card>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
<!-- Profile Button -->
<v-btn
flat
to="/profile"
v-if="user"
>
<v-icon
class="hidden-sm-only"
left
>account_box</v-icon>
<v-badge
right
color="blue darken-2"
:class="{ 'bounce': badgeAnimated }"
>
<span
slot="badge"
v-if="userFavorites.length"
>{{userFavorites.length}}</span>
Profile
</v-badge>
</v-btn>
<!-- Signout Button -->
<v-btn
flat
v-if="user"
@click="handleSignoutUser"
>
<v-icon
class="hidden-sm-only"
left
>exit_to_app</v-icon>
Signout
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view />
</transition>
<!-- Auth Snackbar -->
<v-snackbar
v-model="authSnackbar"
color="success"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">check_circle</v-icon>
<h3>You are now signed in!</h3>
<v-btn
dark
flat
@click="authSnackbar = false"
>Close</v-btn>
</v-snackbar>
<!-- Auth Error Snackbar -->
<v-snackbar
v-if="authError"
v-model="authErrorSnackbar"
color="info"
:timeout='5000'
bottom
left
>
<v-icon class="mr-3">cancel</v-icon>
<h3>{{authError.message}}</h3>
<v-btn
dark
flat
to="/signin"
>Sign in</v-btn>
</v-snackbar>
</v-container>
</main>
</v-app>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
searchTerm: "",
sideNav: false,
authSnackbar: false,
authErrorSnackbar: false,
badgeAnimated: false
};
},
watch: {
user(newValue, oldValue) {
// if we had no value for user before, show snackbar
if (oldValue === null) {
this.authSnackbar = true;
}
},
authError(value) {
// if auth error is not null, show auth error snackbar
if (value !== null) {
this.authErrorSnackbar = true;
}
},
userFavorites(value) {
// if user favorites value changed at all
if (value) {
this.badgeAnimated = true;
setTimeout(() => (this.badgeAnimated = false), 1000);
}
}
},
computed: {
...mapGetters(["searchResults", "authError", "user", "userFavorites"]),
horizontalNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [{ icon: "chat", title: "Posts", link: "/posts" }];
}
return items;
},
sideNavItems() {
let items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
if (this.user) {
items = [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "stars", title: "Create Post", link: "/post/add" },
{ icon: "account_box", title: "Profile", link: "/profile" }
];
}
return items;
}
},
methods: {
handleSearchPosts() {
this.$store.dispatch("searchPosts", {
searchTerm: this.searchTerm
});
},
handleSignoutUser() {
this.$store.dispatch("signoutUser");
},
goToSearchResult(resultId) {
// Clear search term
this.searchTerm = "";
// Go to desired result
this.$router.push(`/posts/${resultId}`);
// Clear search results
this.$store.commit("clearSearchResults");
},
formatDescription(desc) {
return desc.length > 30 ? `${desc.slice(0, 30)}...` : desc;
},
checkIfUserFavorite(resultId) {
return (
this.userFavorites &&
this.userFavorites.some(fave => fave._id === resultId)
);
},
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
h1 {
font-weight: 400;
font-size: 2.5rem;
}
h2 {
font-weight: 400;
font-size: 2rem;
}
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* Search Results Card */
#search__card {
position: absolute;
width: 100vw;
z-index: 8;
top: 100%;
left: 0%;
}
/* User Favorite Animation */
.bounce {
animation: bounce 1s both;
}
@keyframes bounce {
0%,
20%,
53%,
80%,
100% {
transform: translate3d(0, 0, 0);
}
40%,
43% {
transform: translate3d(0, -20px, 0);
}
70% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, -4px, 0);
}
}
</style>
- We are going to modify the
Home.vuepage document to add theExplore PostsButton
client\src\components\Home.vue
<template>
<v-container text-xs-center>
<!-- Loading Spinner -->
<v-layout row>
<v-dialog v-model="loading" persistent fullscreen>
<v-container fill-height>
<v-layout row justify-center align-center>
<v-progress-circular indeterminate :size="70" :width="7" color="secondary"></v-progress-circular>
</v-layout>
</v-container>
</v-dialog>
</v-layout>
<!-- Explore Posts Button -->
<v-layout class="mt-2 mb-3" row wrap v-if="!loading">
<v-flex xs-12>
<v-btn class="secondary" to="/posts" large dark>
Explore Posts
</v-btn>
</v-flex>
</v-layout>
<!-- Posts Carrousel -->
<v-flex xs12>
<v-carousel v-if="!loading && posts.length > 0" v-bind="{ 'cycle': true }" interval="3000">
<v-carousel-item v-for="post in posts" :key="post._id" :src="post.imageUrl" @click.native="goToPost(post._id)">
<h1 id="carousel__title">{{post.title}}</h1>
</v-carousel-item>
</v-carousel>
</v-flex>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "home",
created() {
this.handleGetCarouselPosts();
},
computed: {
...mapGetters(["loading", "posts"])
},
methods: {
handleGetCarouselPosts() {
// reach out to Vuex store, fire action that gets posts for carousel
this.$store.dispatch("getPosts");
},
goToPost(postId) {
this.$router.push(`/posts/${postId}`);
}
}
};
</script>
<style>
#carousel__title {
position: absolute;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px 5px 0 0;
padding: 0.5em;
margin: 0 auto;
bottom: 50px;
left: 0;
right: 0;
}
</style>
- We are going to modify the
Auth\Profile.vueprofile page document to allow to go to the Posts from the Post Images.
client\src\components\Auth\Profile.vue
<template>
<v-container class="text-xs-center">
<!-- User Details Card -->
<v-flex
sm6
offset-sm3
>
<v-card
class="white--text"
color="secondary"
>
<v-layout>
<v-flex xs5>
<v-img
height="125px"
contain
:src="user.avatar"
></v-img>
</v-flex>
<v-flex xs7>
<v-card-title primary-title>
<div>
<div class="headline">{{user.username}}</div>
<div>Joined {{formatJoinDate(user.joinDate)}}</div>
<div class="hidden-xs-only font-weight-thin">{{user.favorites.length}} Favorites</div>
<div class="hidden-xs-only font-weight-thin">{{userPosts.length}} Posts Added</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
<!-- Posts Favorited by User -->
<v-container v-if="!userFavorites.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no favorites currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Favorited
<span class="font-weight-regular">({{userFavorites.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="favorite in userFavorites"
:key="favorite._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-img
height="30vh"
:src="favorite.imageUrl"
@click="goToPost(favorite._id)"
></v-img>
<v-card-text>{{favorite.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Posts Created By user -->
<v-container v-if="!userPosts.length">
<v-layout
row
wrap
>
<v-flex xs12>
<h2>You have no posts currently. Go and add some!</h2>
</v-flex>
</v-layout>
</v-container>
<v-container
class="mt-3"
v-else
>
<v-flex xs12>
<h2 class="font-weight-light">Your Posts
<span class="font-weight-regular">({{userPosts.length}})</span>
</h2>
</v-flex>
<v-layout
row
wrap
>
<v-flex
xs12
sm6
v-for="post in userPosts"
:key="post._id"
>
<v-card
class="mt-3 ml-1 mr-2"
hover
>
<v-btn
color="info"
floating
fab
small
dark
@click="loadPost(post)"
>
<v-icon>edit</v-icon>
</v-btn>
<v-btn
color="error"
floating
fab
small
dark
@click="handleDeleteUserPost(post)"
>
<v-icon>delete</v-icon>
</v-btn>
<v-img
height="30vh"
:src="post.imageUrl"
@click="goToPost(post._id)"
></v-img>
<v-card-text>{{post.title}}</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
<!-- Edit Post Dialog -->
<v-dialog
xs12
sm6
offset-sm3
persistent
v-model="editPostDialog"
>
<v-card>
<v-card-title class="headline grey lighten-2">Update Post</v-card-title>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleUpdateUserPost"
>
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="titleRules"
v-model="title"
label="Post Title"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="imageRules"
v-model="imageUrl"
label="Image URL"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<!-- Image Preview -->
<v-layout row>
<v-flex xs12>
<img
:src="imageUrl"
height="300px"
>
</v-flex>
</v-layout>
<!-- Categories Select -->
<v-layout row>
<v-flex xs12>
<v-select
v-model="categories"
:rules="categoriesRules"
:items="['Art', 'Education', 'Food', 'Furniture', 'Travel', 'Photography', 'Technology']"
multiple
label="Categories"
></v-select>
</v-flex>
</v-layout>
<!-- Description Text Area -->
<v-layout row>
<v-flex xs12>
<v-textarea
:rules="descRules"
v-model="description"
label="Description"
type="text"
required
></v-textarea>
</v-flex>
</v-layout>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="!isFormValid"
type="submit"
class="success--text"
flat
>Update</v-btn>
<v-btn
class="error--text"
flat
@click="editPostDialog = false"
>Cancel</v-btn>
</v-card-actions>
</v-form>
</v-container>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import moment from "moment";
import { mapGetters } from "vuex";
export default {
name: "Profile",
data() {
return {
editPostDialog: false,
isFormValid: true,
title: "",
imageUrl: "",
categories: [],
description: "",
titleRules: [
title => !!title || "Title is required",
title => title.length < 20 || "Title must have less than 20 characters"
],
imageRules: [image => !!image || "Image is required"],
categoriesRules: [
categories =>
categories.length >= 1 || "At least one category is required"
],
descRules: [
desc => !!desc || "Description is required",
desc =>
desc.length < 200 || "Description must have less than 200 characters"
]
};
},
computed: {
...mapGetters(["user", "userFavorites", "userPosts"])
},
created() {
this.handleGetUserPosts();
},
methods: {
goToPost(id) {
this.$router.push(`/posts/${id}`);
},
formatJoinDate(date) {
return moment(new Date(date)).format("ll");
},
handleGetUserPosts() {
this.$store.dispatch("getUserPosts", {
userId: this.user._id
});
},
handleUpdateUserPost() {
if (this.$refs.form.validate()) {
this.$store.dispatch("updateUserPost", {
postId: this.postId,
userId: this.user._id,
title: this.title,
imageUrl: this.imageUrl,
categories: this.categories,
description: this.description
});
this.editPostDialog = false;
}
},
handleDeleteUserPost(post) {
this.loadPost(post, false);
const deletePost = window.confirm(
"Are you sure you want to delete this post?"
);
if (deletePost) {
this.$store.dispatch("deleteUserPost", {
postId: this.postId
});
}
},
loadPost(
{ _id, title, imageUrl, categories, description },
editPostDialog = true
) {
this.editPostDialog = editPostDialog;
this.postId = _id;
this.title = title;
this.imageUrl = imageUrl;
this.categories = categories;
this.description = description;
}
}
};
</script>
- We are going to modify the
Posts\Posts.vueposts page document to manage properly theshowMoreEnabledproperty.
client\src\components\Posts\Posts.vue
<template>
<v-container fluid grid-list-xl>
<!-- Post Cards -->
<v-layout row wrap v-if="infiniteScrollPosts">
<v-flex xs12 sm6 v-for="post in infiniteScrollPosts.posts" :key="post._id">
<v-card hover>
<v-img @click.native="goToPost(post._id)" :src="post.imageUrl" height="30vh" lazy></v-img>
<v-card-actions>
<v-card-title primary>
<div>
<div class="headline">{{post.title}}</div>
<span class="grey--text">{{post.likes}} likes - {{post.messages.length}} comments</span>
</div>
</v-card-title>
<v-spacer></v-spacer>
<v-btn @click="showPostCreator = !showPostCreator" icon>
<v-icon>{{`keyboard_arrow_${showPostCreator ? 'up' : 'down'}`}}</v-icon>
</v-btn>
</v-card-actions>
<!-- Post Creator Tile -->
<v-slide-y-transition>
<v-card-text v-show="showPostCreator" class="grey lighten-4">
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="post.createdBy.avatar">
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title class="text--primary">{{post.createdBy.username}}</v-list-tile-title>
<v-list-tile-sub-title class="font-weight-thin">Added {{formatCreatedDate(post.createdDate)}}</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon ripple>
<v-icon color="grey lighten-1">info</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-card-text>
</v-slide-y-transition>
</v-card>
</v-flex>
</v-layout>
<!-- Fetch More Button -->
<v-layout v-if="showMoreEnabled" column>
<v-flex xs12>
<v-layout justify-center row>
<v-btn color="info" @click="showMorePosts">Fetch More</v-btn>
</v-layout>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import moment from "moment";
import { INFINITE_SCROLL_POSTS } from "../../queries";
const pageSize = 2;
export default {
name: "Posts",
data() {
return {
pageNum: 1,
// showMoreEnabled: true,
showPostCreator: false
};
},
apollo: {
infiniteScrollPosts: {
query: INFINITE_SCROLL_POSTS,
variables: {
pageNum: 1,
pageSize
}
}
},
computed: {
showMoreEnabled() {
return this.infiniteScrollPosts && this.infiniteScrollPosts.hasMore;
}
},
methods: {
formatCreatedDate(date) {
return moment(new Date(date)).format("ll");
},
showMorePosts() {
this.pageNum += 1;
// fetch more data and transform original result
this.$apollo.queries.infiniteScrollPosts.fetchMore({
variables: {
// pageNum incremented by 1
pageNum: this.pageNum,
pageSize
},
updateQuery: (prevResult, { fetchMoreResult }) => {
console.log("previous result", prevResult.infiniteScrollPosts.posts);
console.log("fetch more result", fetchMoreResult);
const newPosts = fetchMoreResult.infiniteScrollPosts.posts;
const hasMore = fetchMoreResult.infiniteScrollPosts.hasMore;
// this.showMoreEnabled = hasMore;
return {
infiniteScrollPosts: {
__typename: prevResult.infiniteScrollPosts.__typename,
// Merge previous posts with new posts
posts: [...prevResult.infiniteScrollPosts.posts, ...newPosts],
hasMore
}
};
}
});
},
goToPost(postId) {
this.$router.push(`/posts/${postId}`);
}
}
};
</script>
- We need to test if everything works properly.






Section 16: Deployment with Heroku / Now v2 0 / 1|13min
83. Deploying App with Heroku / Now v2 13min
We are going to use Heroku to deploy our
server codeand Now to deploy ourclient code.Signup on
Keroku

- Signup on
Now


- We need to set up the Atlas
IP Whitelist.

Deploy the server code
- We need to modify the
server.jsdocument to set up theserver port value
server.js
const { ApolloServer, AuthenticationError } = require("apollo-server");
const mongoose = require("mongoose");
const fs = require("fs");
const path = require("path");
const jwt = require("jsonwebtoken");
// Import Environment variables from .env
require("dotenv").config();
// Import typedefs and resolvers
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const resolvers = require("./resolvers");
const User = require("./models/User");
const Post = require("./models/Post");
// Connect to MongoDb Atlas
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
// Verify JWT Token passed from client
const getUser = async token => {
if (token) {
try {
return await jwt.verify(token, process.env.SECRET);
} catch (err) {
throw new AuthenticationError(
"Your session has ended. Please sign in again."
);
}
}
};
// Create Apollo/GraphQL Server using typeDefs, resolvers, and context object
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: error => ({
name: error.name,
message: error.message.replace("Context creation failed:", "")
}),
context: async ({ req }) => {
const token = req.headers["authorization"];
return { User, Post, currentUser: await getUser(token) };
}
});
server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We need to create a new App on
Heroku.

- Put the name:
vue-with-graphql-peelmicroand then click onCreate app

- Install the
Heroku CLI
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm i -g heroku
npm WARN deprecated cross-spawn-async@2.2.5: cross-spawn no longer requires a build toolchain, use it instead
C:\Users\juan.pablo.perez\AppData\Roaming\npm\heroku -> C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\heroku\bin\run
+ heroku@7.27.1
added 26 packages from 12 contributors, removed 5 packages, updated 48 packages and moved 6 packages in 126.305sheroku login
- Log in from the terminal to
Heroku
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ heroku login
heroku: Press any key to open up the browser to login or q to exit:

Opening browser to https://cli-auth.heroku.com/auth/browser/d99aa950-7e75-4a3e-bca6-717446cd370b
heroku: Waiting for login... |

Logging in... done
Logged in as juanp_perez@msn.com
Create a
gitrepository, if it is not created yet, usinggit initWe need to commit the changes.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ git commit -m "Heroku Deployment"
[master 0016d2e] Heroku Deployment
1 file changed, 1 insertion(+), 1 deletion(-)
- Set the
Herokuremote repository
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ heroku git:remote -a vue-with-graphql-peelmicro
set git remote heroku to https://git.heroku.com/vue-with-graphql-peelmicro.git
- Include the environment variables on
Heroku

- Deploy on
Heroku
juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ git push heroku master
Enumerating objects: 190, done.
Counting objects: 100% (190/190), done.
Delta compression using up to 4 threads
Compressing objects: 100% (187/187), done.
Writing objects: 100% (190/190), 138.99 KiB | 837.00 KiB/s, done.
Total 190 (delta 102), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote: NPM_CONFIG_LOGLEVEL=error
remote: NODE_ENV=production
remote: NODE_MODULES_CACHE=true
remote: NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote: engines.node (package.json): unspecified
remote: engines.npm (package.json): unspecified (use default)
remote:
remote: Resolving node version 10.x...
remote: Downloading and installing node 10.16.3...
remote: Using default npm version: 6.9.0
remote:
remote: -----> Installing dependencies
remote: Installing node modules (package.json + package-lock)
remote:
remote: > bcrypt@3.0.6 install /tmp/build_4390a153baa002773690df5c5d1656a7/node_modules/bcrypt
remote: > node-pre-gyp install --fallback-to-build
remote:
remote: [bcrypt] Success: "/tmp/build_4390a153baa002773690df5c5d1656a7/node_modules/bcrypt/lib/binding/bcrypt_lib.node" is installed via remote
remote:
remote: > core-js@3.1.3 postinstall /tmp/build_4390a153baa002773690df5c5d1656a7/node_modules/core-js
remote: > node scripts/postinstall || echo "ignore"
remote:
remote:
remote: > protobufjs@6.8.8 postinstall /tmp/build_4390a153baa002773690df5c5d1656a7/node_modules/protobufjs
remote: > node scripts/postinstall
remote:
remote:
remote: > nodemon@1.19.1 postinstall /tmp/build_4390a153baa002773690df5c5d1656a7/node_modules/nodemon
remote: > node bin/postinstall || exit 0
remote:
remote: Love nodemon? You can now support the project via the open collective:
remote: > https://opencollective.com/nodemon/donate
remote:
remote: added 475 packages from 358 contributors and audited 3222 packages in 11.936s
remote: found 0 vulnerabilities
remote:
remote:
remote: -----> Build
remote:
remote: -----> Pruning devDependencies
remote: removed 229 packages and audited 912 packages in 3.814s
remote: found 0 vulnerabilities
remote:
remote:
remote: -----> Caching build
remote: - node_modules
remote:
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote: Procfile declares types -> (none)
remote: Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote: Done: 24.7M
remote: -----> Launching...
remote: Released v5
remote: https://vue-with-graphql-peelmicro.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/vue-with-graphql-peelmicro.git
* [new branch] master -> master
- We should ensure it is working browsing https://vue-with-graphql-peelmicro.herokuapp.com/

Deploy the client code
- As we need to make changes on the app that are going to make the app not work locally we are going to create a new branch.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ git checkout -b NowDeployment
Switched to a new branch 'NowDeployment'
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (NowDeployment)
$ git status
On branch NowDeployment
nothing to commit, working tree clean
- Modify the
main.jsdocument to change theApollo Client urivalue.
client\src\main.js
// import "@babel/polyfill"
import Vue from "vue"
import "./plugins/vuetify"
import App from "./App.vue"
import router from "./router"
import store from "./store"
import ApolloClient from "apollo-boost"
import VueApollo from "vue-apollo"
import FormAlert from "./components/Shared/FormAlert"
// Register Global Component
Vue.component("form-alert", FormAlert)
Vue.use(VueApollo)
// Setup ApolloClient
export const defaultClient = new ApolloClient({
uri: "https://vue-with-graphql-peelmicro.herokuapp.com/graphql",
// include auth token with requests made to backend
fetchOptions: {
credentials: "include"
},
request: operation => {
// if no token with key of 'token' in localStorage, add it
if (!localStorage.token) {
localStorage.setItem("token", "")
}
// operation adds the token to an authorization header, which is sent to backend
operation.setContext({
headers: {
authorization: localStorage.getItem("token")
}
})
},
onError: ({ graphQLErrors, networkError }) => {
if (networkError) {
console.log("[networkError]", networkError)
}
if (graphQLErrors) {
for (let err of graphQLErrors) {
console.dir(err)
if (err.name === "AuthenticationError") {
// set auth error in state (to show in snackbar)
store.commit("setAuthError", err);
// signout user (to clear token)
store.dispatch("signoutUser");
}
}
}
}
})
const apolloProvider = new VueApollo({ defaultClient })
Vue.config.productionTip = false
new Vue({
apolloProvider,
router,
store,
render: h => h(App),
created() {
// execute getCurrentUser query
this.$store.dispatch("getCurrentUser")
}
}).$mount("#app")
- We need to install the graphql-tag and GraphQL.js packages.
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (NowDeployment)
$ npm i graphql-tag graphql
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ graphql-tag@2.10.1
+ graphql@14.4.2
added 2 packages from 1 contributor and updated 1 package in 42.542s
We need to install our
VueJSapplication as a set of static files. We need thenow.jsondocument to be able to do that.We can find the original code that we need on GitHub Gist CodeArtistry.io.
client\now.json
{
"version": 2,
"name": "vue-with-graphql-peelmicro",
"builds": [{ "src": "package.json", "use": "@now/static-build" }],
"routes": [
{ "src": "^/js/(.*)", "dest": "/js/$1" },
{ "src": "^/css/(.*)", "dest": "/css/$1" },
{ "src": "^/img/(.*)", "dest": "/img/$1" },
{ "src": ".*", "dest": "/index.html" }
]
}
- We need to modify the
client\package.jsondocument to add the,"now-build": "npm run build"script.
client\package.json
{
"name": "client",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"now-build": "npm run build"
},
"dependencies": {
"apollo-boost": "^0.4.3",
"core-js": "^2.6.5",
"graphql": "^14.4.2",
"graphql-tag": "^2.10.1",
"lodash": ">=4.17.13",
"moment": "^2.24.0",
"vue": "^2.6.10",
"vue-apollo": "^3.0.0-rc.1",
"vue-router": "^3.0.3",
"vuetify": "^1.5.5",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vue-cli-plugin-vuetify": "^0.5.0",
"vue-template-compiler": "^2.6.10",
"vuetify-loader": "^1.0.5"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
- We need to install the
Now CLIby usingnpm i -g now
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (NowDeployment)
$ npm i -g now
> now@16.1.1 preinstall C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\now
> node ./scripts/preinstall.js
C:\Users\juan.pablo.perez\AppData\Roaming\npm\now -> C:\Users\juan.pablo.perez\AppData\Roaming\npm\node_modules\now\dist\index.js
+ now@16.1.1
added 1 package in 179.252s
- We need to login on
Nowby usingnow login

Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (NowDeployment)
$ now login
> We sent an email to juanp_perez@loyaltycrm.com. Please follow the steps provided
inside it and make sure the security code matches Jolly Quoll.


Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (NowDeployment)
$ now login
> We sent an email to juanp_perez@loyaltycrm.com. Please follow the steps provided
inside it and make sure the security code matches Jolly Quoll.
√ Email confirmed
> Ready! Authentication token and personal details saved in "~\.now"
- We can deploy our application by using
now
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide/client (NowDeployment)
$ now
> Deploying C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client under peelmicro
> Using project vue-with-graphql-peelmicro
> Synced 14 files (421.16KB) [2s]
> https://vue-with-graphql-peelmicro-ibnbb83kh.now.sh [v2] [876ms]
> Ready! Deployed to https://vue-with-graphql-peelmicro.peelmicro.now.sh [in clipboard] [2m]
- We can check if it works.

DeployingAppWithHerokuNowV36
DeployingAppWithHerokuNowV37
DeployingAppWithHerokuNowV38
DeployingAppWithHerokuNowV39
DeployingAppWithHerokuNowV40DeployingAppWithHerokuNowV36