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

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 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.

  • Vetur

  • Change Vetur › Format › Default Formatter: HTML to js-beautify-html

  • Ensure editor: format on save is true

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 GraphQL requests.

  • We only want the name, height and mass. We graphQL is 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 helps do 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-guide folder 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.js document to set up the Apollo 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.js to 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 Playground is open.

  • We need to modify the server.js to 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.js to 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 mutations as 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 getTodos Query 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 curl command of our query or mutation by clicking Copy Curl button. It will copy it to the clipboard
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 variables by clicking QUERY VARIABLES

  • We can manage the Http Headers by clicking HTTTP HEADERS

  • We can add multiple Queries and Mutations by clicking the + button

  • We can export the Schema by clicking SCHEMA and the DOWNLOAD

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 gear icon.

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 Queries and mutations by clicking HISTORY

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.js to remove

  • Click on Connect your Application

  • Copy the connection String

  • We need to create a new .env document where we are going to put the environment 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.js to connect Mongo 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 new models folder and the Post.js and User.js model 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.js to import the models.

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.gql document that will contain the all the typeDefs included 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.js to import the typeDefs.

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.gql document to include the getUser query and the signupUser mutation.

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.js document that is going to be used to resolve the query and the mutation.

. 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.js document to import the resolvers document.

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.gql document to add the addPost mutation.

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.js document to add the resolver for the addPost mutation.

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 Post using the addPost mutation.

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.gql document to add the getPosts query.

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.js document to add the resolver for the getPosts query.

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 getPosts query 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.js version 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 Interface by executing vue 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 client for Project folder, npm for Package manager, unselect Initilize git repository (recommender) for Git repository and click on Next

  • Select Manual for Select a preset and click on Next

  • Select Router, Vuex, unselect Linter / Formatter and click on Next

  • Select Use history moder for Router and click on Create 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 plugins we can see the plugins already installed.

  • If we click on +Add plugin we can see a list of available plugins

  • We missed adding Vuex, so we can do it by clicking on Add Vuex button.

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.json to the client script from cd client && npm start to cd client && npm run serve because it is how the script is called on the client package.json document.

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, both server and client are 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.vue to client\src\components\Home.vue and then remove the client\src\views folder. We are also going to remove the src\assets folder and the src\components\HelloWorld.vue document.

  • We are going to modify the client\src\components\Home.vue document

client\src\components\Home.vue

<template>
  <div>
    <h1>Home</h1>
  </div>
</template>

<script>
export default {
  name: "home"
};
</script>

  • The client\src\main.js document 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.js document.

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.js document 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 Vuetify by using Vue CLI-3

  • We are going to install it by using the vue add vuetify command.
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.js has 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.html has 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 are going to modify the client\src\plugins\vuetify.js document to add the theme colours`.

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.vue document to add a button to see if the new theme works.

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.vue document 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.vue document to create the layout for our app using Vuetify

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.vue document 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.vue document to add a Side Navbarp using Vuetify

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\Auth folder with the Profile.vue, Signin.vue and Signup.vue documents 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\Posts folder with the AddPost.vue and Posts.vue documents 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.js to include the new components

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.vue document to add Page 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-boost and the vue-apollo packages.
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.js document 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.vue document to include all the posts

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.gpl document to include the Id field for the Post type.

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.vue document to make a loading... test show when Apollo is 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 result method of Apollo to get information from the response and store it in a Vue.JS property. There are some other methods like networkStatus.

\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 result method 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 error where we can obtain all the errors that can happen when executing the GraphQL query.

\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>

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>

  • We are going to use the Corousel Vuetify components.

\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.Vue component, the main.js main document and the store.js store document to use the Vuex store to call the GraphQL getPosts query.

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.Vue component and the store.js store 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.Vue component and the store.js store 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.js to 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.js store document again to use the new queries.js document.

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.js document 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 mutation to 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 signinUser and signupUser mutations 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 SECRET environment 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 signinUser mutation.

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 signupUser mutation.

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 signupUser mutation.

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.js document in the client project to add the new signin and signup mutations.

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 signinUser action.

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.vue component to make a proper Signin form.

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 localStorage contains the token.

  • We are going to modify in the client project the main.js document to manage how to send the token automatically to our GraphQL queries and mutations.

\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 server method in the server.js document.

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 server console.
[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.js document 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 AuthenticationError class 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 Query by modifying the typeDefs.gql and resolvers.js documents.

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.js document to create the user state, the setUser mutation, the getCurrentUser action and the user getter.

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.js document to include the GET_CURRENT_USER user QUERY

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.js document to force to execute the getCurrentUser action 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 Signin component to redirect to home page if the data received from the user getter 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.vue document to put the Navbar routes based 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.js document to create a mutation and an action to be able to sign out a 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.vue page to include the functionaly to sign out a 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.js document 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.js document to use the new AuthGuard.js document.

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.js document 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.js document to include the error state.

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 Shared folder from the client\src\components and the FormAlert.vue component 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.vue document page to use the new FormAlert component and the new Error state.

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 FormAlert component works properly.

47. Add Loading Spinner to Signin Button 3min

  • We need to modify the Signin.vue page document to include a Vuetify loading 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.vue page document to include the Vuetify form 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.vue main page document to add a AuthSnackbar Vuetify 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.js document 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.js document to include the new AuthError state.

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.js document to update the AuthError state 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.vue main page document to add another AuthSnackbar Vuetify 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.js to add the new signupUser action.

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.vue page document to include all the functionality to manage the Signup.

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.vue page document to include the form that it's going to allow us to create a new post.

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"