Full-Stack Vue with GraphQL - The Ultimate Guide
Github Repositories
The Full-Stack Vue with GraphQL - The Ultimate Guide Udemy course explains how to Build a complete Pinterest-inspired full-stack app from scratch with Vue, GraphQL, Apollo 2, Vuex, and Vuetify.
Table of contents
- What I've learned
- Section 1: Introduction 0 / 3|29min
- Section 2: What is GraphQL / Apollo? (Optional) 0 / 2|23min
- Section 3: Intro to Apollo Server 2, Queries, Mutations and GraphQL Playground 0 / 5|27min
- Section 4: Connect to MLab Database, Create Mongoose Models and GraphQL TypeDefs 0 / 7|54min
- 11. Create MongoDB Atlas Database, Connect to GraphQL Server 7min
- 12. Update! Connecting to MongoDB Atlas instead of MLab 4min
- 13. Creating Mongoose Schemas 11min
- 14. Creating typeDefs for Project 8min
- 15. Write and Run signupUser Mutation 9min
- 16. Write and Run addPost Mutation 7min
- 17. Write and Run getPosts Query, Intro to populate 7min
- Section 5: Create Vue Frontend with Vue CLI 3 0 / 8|57min
- 18. Create Vue Client with Vue-CLI 3 8min
- 19. Adding Plugins with Vue GUI and Concurrently Dev Script 4min
- 20. Structuring our Vue App 4min
- 21. Installing Vuetify Plugin and Generating a Theme 10min
- 22. Coolors.co for Creating Great Color Schemes (Optional) 4min
- 23. Horizontal Navbar and Mobile First Design 12min
- 24. Add Side Navbar 7min
- 25. Add Routing and Page Transitions 9min
- Section 6: Using Vue Apollo 0 / 4|24min
- Section 7: Integrate Vuex with ApolloClient 0 / 4|23min
- Section 8: JWT Authentication for Signin / Signup 0 / 12|1hr 27min
- 34. Create Gravatar Avatar and Hash User Passwords on Signup 7min
- 35. Write and Run signinUser Mutation 5min
- 36. Sign Token and Return it Upon Signin/Signup 8min
- 37. Using Variables in GraphQL, Signin / Signup Mutation Defs 6min
- 38. Add Signin Form, Write and Run signinUser Action, Return JWT 9min
- 39. Additional Config for ApolloClient, Send Token from LocalStorage 8min
- 40. Verify JWT Token in server.js, Pass Result to currentUser in Context 7min
- 41. Create getCurrentUser Query, Execute it from main.js 9min
- 42. Redirect Home upon Signin with Watcher 8min
- 43. Change Navbar for Signed-in User 9min
- 44. Create Signout Action 6min
- 45. Protected Routes and Clearing Malformed Tokens 5min
- Section 9: Error Handling and Form Validation 0 / 6|43min
- 46. Adding Global Form Alert Component 9min
- 47. Add Loading Spinner to Signin Button 3min
- 48. Form Validation with Vuetify in Signin Component 7min
- 49. Show AuthSnackbar on Signin / Signup 5min
- 50. Handle Authentication Errors, Show Auth Error Snackbar 9min
- 51. Create Signup Form and Signup User Action 11min
- Section 10: Add Post / Infinite Scroll Components 0 / 6|58min
- 52. Make Add Post Form 8min
- 53. Create and Execute addPost Action / Mutation 10min
- 54. Update and Optimistic Response for addPost Mutation 11min
- 55. Infinite Scroll on Posts Page; Add typeDef, Resolver, and Query 8min
- 56. Add Infinite Scroll Functionality on Client 10min
- 57. Add Grid Layout / Cards for Each Post in Posts Component 10min
- Section 11: Post Component 0 / 7|44min
- 58. Create Post Component and Route 5min
- 59. Create and Execute getPost Query 7min
- 60. Build out Post Card in Post Component 8min
- 61. Add Messages ## Section to Post Component 6min
- 62. Create addPostMessage Mutation 6min
- 63. Perform addPostMessage in Post Component 8min
- 64. Add Validation for Message Input, Clear on Submit 5min
- Section 12: Like / Unlike Post 0 / 4|27min
- Section 13: Search Posts 0 / 4|22min
- Section 14: Profile Page, Update / Delete Posts 0 / 7|53min
- 73. Add User Details Card / Favorites Cards 6min
- 74. Write getUserPosts Query 4min
- 75. Execute getUserPosts Query, Create and Populate User Cards 9min
- 76. Add Edit Post Dialog for Updating User Posts 6min
- 77. Create updateUserPost Mutation 8min
- 78. Executing updateUserPost Mutation with Vuex Action 13min
- 79. deleteUserRecipe Mutation - Backend Creation to Frontend Execution 7min
- Section 15: Preparing for Deployment 0 / 3|17min
- Section 16: Deployment with Heroku / Now v2 0 / 1|13min
- Section 17: BONUS 0 / 1|1min
Related projects
Project | Dates | Source Code |
---|---|---|
NestJs and Nuxt version of the 'Full-Stack Vue with GraphQL - The Ultimate Guide' course | 19/8/2019 | full-stack-vue-with-graphql-the-ultimate-guide-nuxt |
What I've learned
- Learn in-depth how to use Apollo Server 2 and Apollo Boost to create powerful full-stack apps
- Learn how to handle errors on the client and server with Apollo / GraphQL
- Be able to implement session-based JWT authentication to GraphQL applications
- Integrate Apollo with Vuex for more reliable and scalable state management
- Implement infinite scrolling functionality using Vue-Apollo
- Deploy full-stack JavaScript / GraphQL applications using Heroku and Netlify
- Learn how to write queries and mutations in the GraphQL language on both the client and server
- Make use of many useful MongoDB methods and features
- Be able to create attractive, sophisticated UIs using the Vuetify CSS framework
- Become more familiar with all the best ES6 / 7 features such as async / await, destructuring, spread operators, arrow functions, etc
Section 1: Introduction 0 / 3|29min
1. Preview our Completed App 18min
2. Tools Used/Required 4min
- We are going to use NodeJs.
- We are also going to use Visual Studio Code
- We'll be using MongoDB Atlas
- We'll also be using Now — Global Serverless Deployments
- We'll be using Chrome Canary
- We need to install
Vue.js devtools
3. Formatting Vue Templates (And More) with VS Code 6min
We need to install the following Visual Studio Extensions.
- Change
Vetur › Format › Default Formatter: HTML
tojs-beautify-html
- Ensure
editor: format on save
istrue
- We are going to install Prettier - Code formatter
- We are also going to install Graphql For VSCode
Section 2: What is GraphQL / Apollo? (Optional) 0 / 2|23min
4. What is GraphQL? Using the SWAPI GraphQL API 20min
- We are going to use the [SWAPI The Star Wars AP][https://www.swapi.co/] web site
request
https://swapi.co/api/people/1/`
response
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": ["https://swapi.co/api/species/1/"],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
}
We cannot filter by specific values or show only the data that we need.
We can use GraphiQL to make
GraphQL
requests.
- We only want the
name
,height
andmass
. WegraphQL
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 helpsdo GraphQl Right
. It's a single versatile query system to replace a patchwork of legacy APIs, with all the devtools and cloud services you need to run GraphQL at scale.
We are going to work with Apollo Server that is the best way to quickly build a production-ready, self-documenting API for GraphQL clients, using data from any source.
We are also going to work with Apollo Boost that is a great way to get started with Apollo Client quickly, but there are some advanced features it doesn't support out of the box, so with Apollo Boost migration we need to set Apollo Client up manually.
We are also going to work with Apollo and GraphQL for Vue.js that helps integrate GraphQL in your Vue.js apps!
Section 3: Intro to Apollo Server 2, Queries, Mutations and GraphQL Playground 0 / 5|27min
6. Git Clone and Install Dependencies (Required) 2min
We are going to need to clone the https://github.com/reedbarger/fullstack-vue-graphql-starter repository to start developing the server side of our application.
Create the
full-stack-vue-with-graphql-the-ultimate-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 theApollo Server
server.js
const { ApolloServer, gql } = require("apollo-server");
const server = new ApolloServer({});
server.listen();
- We are going to execute the server
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm run server
> fullstack-vue-graphql-starter@1.0.0 server C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> nodemon server.js
[nodemon] 1.19.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node server.js`
C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-core\dist\ApolloServer.js:139
throw Error('Apollo Server requires either an existing schema, modules or typeDefs');
^
Error: Apollo Server requires either an existing schema, modules or typeDefs
at new ApolloServerBase (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-core\dist\ApolloServer.js:139:23)
at new ApolloServer (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server-express\dist\ApolloServer.js:46:9)
at new ApolloServer (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\node_modules\apollo-server\dist\index.js:23:9)
at Object.<anonymous> (C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\server.js:3:16)
at Module._compile (internal/modules/cjs/loader.js:805:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:816:10)
at Module.load (internal/modules/cjs/loader.js:672:32)
at tryModuleLoad (internal/modules/cjs/loader.js:612:12)
at Function.Module._load (internal/modules/cjs/loader.js:604:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:868:12)
[nodemon] app crashed - waiting for file changes before starting...
- We need to modify the
server.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 clickingCopy Curl
button. It will copy it to theclipboard
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ curl 'http://localhost:4000/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Connection: keep-al
ive' -H 'DNT: 1' -H 'Origin: http://localhost:4000' --data-binary '{"query":"query {\n getTodos {\n task\n completed\n }\n}\n"}' --compressed
{"data":{"getTodos":[{"task":"Wash car","completed":false},{"task":"Clean room","completed":true},{"task":"Eat lunch","completed":true}]}}
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
- We can manage
Query variables
by clickingQUERY VARIABLES
- We can manage the
Http Headers
by clickingHTTTP HEADERS
- We can add multiple
Queries
andMutations
by clicking the+
button
- We can export the
Schema
by clickingSCHEMA
and theDOWNLOAD
instrospectionSchema.json
{
"_queryType": "Query",
"_mutationType": "Mutation",
"_subscriptionType": null,
"_directives": [
{
"name": "cacheControl",
"description": "",
"locations": ["FIELD_DEFINITION", "OBJECT", "INTERFACE"],
"args": [
{
"name": "maxAge",
"description": "",
"type": "Int"
},
{
"name": "scope",
"description": "",
"type": "CacheControlScope"
}
]
},
{
"name": "skip",
"description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"args": [
{
"name": "if",
"description": "Skipped when true.",
"type": "Boolean!"
}
]
},
{
"name": "include",
"description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
"locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"],
"args": [
{
"name": "if",
"description": "Included when true.",
"type": "Boolean!"
}
]
},
{
"name": "deprecated",
"description": "Marks an element of a GraphQL schema as no longer supported.",
"locations": ["FIELD_DEFINITION", "ENUM_VALUE"],
"args": [
{
"name": "reason",
"description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).",
"type": "String",
"defaultValue": "No longer supported"
}
]
}
],
"astNode": null,
"_typeMap": {
"Query": "Query",
"Todo": "Todo",
"String": "String",
"Boolean": "Boolean",
"Mutation": "Mutation",
"__Schema": "__Schema",
"__Type": "__Type",
"__TypeKind": "__TypeKind",
"__Field": "__Field",
"__InputValue": "__InputValue",
"__EnumValue": "__EnumValue",
"__Directive": "__Directive",
"__DirectiveLocation": "__DirectiveLocation",
"CacheControlScope": "CacheControlScope",
"Upload": "Upload",
"Int": "Int"
},
"_implementations": {}
}
schema.graphql
directive @cacheControl(
maxAge: Int
scope: CacheControlScope
) on FIELD_DEFINITION | OBJECT | INTERFACE
enum CacheControlScope {
PUBLIC
PRIVATE
}
type Mutation {
addTodo(task: String, completed: Boolean): Todo
}
type Query {
getTodos: [Todo]
}
type Todo {
task: String
completed: Boolean
}
# The `Upload` scalar type
represents a file upload.
scalar Upload
- We can modifiy the setting by clicking on the
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
andmutations
by clickingHISTORY
Section 4: Connect to MLab Database, Create Mongoose Models and GraphQL TypeDefs 0 / 7|54min
11. Create MongoDB Atlas Database, Connect to GraphQL Server 7min
- We are going to connect
MongoDB Altas
- Connect to
cluster0
Modify the
server.js
to removeClick on
Connect your Application
- Copy the connection String
- We need to create a new
.env
document where we are going to put theenvironment variables
.env
MONGO_URI=mongodb+srv://USERNAME:PASSWORD@CLUSTERID.mongodb.net/DATABASENAME?retryWrites=true&w=majority
Note:
- this document must be added to the .gitignore file.We need to modify the
server.js
to connectMongo Atlas
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
mongoose
.connect(process.env.MONGO_URI, { useNewUrlParser: true })
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const server = new ApolloServer({
typeDefs
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
- We can try now to run the app
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ npm start dev
> fullstack-vue-graphql-starter@1.0.0 start C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide
> node server.js "dev"
Server listening on http://localhost:4000/
DB connected
12. Update! Connecting to MongoDB Atlas instead of MLab 4min
13. Creating Mongoose Schemas 11min
- We are going to create the models for the
MongoDb
, starting by creating the newmodels
folder and thePost.js
andUser.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 themodels
.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
const User = require("./models/User");
const Post = require("./models/Post");
mongoose
.connect(process.env.MONGO_URI, { useNewUrlParser: true })
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const typeDefs = gql`
type Todo {
task: String
completed: Boolean
}
type Query {
getTodos: [Todo]
}
`;
const server = new ApolloServer({
typeDefs,
context: {
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
14. Creating typeDefs for Project 8min
- We are going to create the
typeDefs.gql
document that will contain the all thetypeDefs
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 thetypeDefs
.
server.js
const { ApolloServer, gql } = require("apollo-server");
const mongoose = require("mongoose");
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const filePath = path.join(__dirname, "typeDefs.gql");
const typeDefs = fs.readFileSync(filePath, "utf-8");
const User = require("./models/User");
const Post = require("./models/Post");
mongoose
.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useCreateIndex: true
})
.then(() => console.log("DB connected"))
.catch(error => console.error(error));
const server = new ApolloServer({
typeDefs,
context: {
User,
Post
}
});
server.listen().then(({ url }) => {
console.log(`Server listening on ${url}`);
});
15. Write and Run signupUser Mutation 9min
- We are going to modify the
typeDefs.gql
document to include thegetUser
query and thesignupUser
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 thequery
and themutation
.
. resolvers.js
module.exports = {
Query: {
getUser: () => null
},
Mutation: {
signupUser: async (_, { username, email, password }, { User }) => {
const user = await User.findOne({ username });
if (user) {
throw new Error("User already exists");
}
const newUser = await new User({
username,
email,
password
}).save();
return newUser;
}
}
};
- We are going to modify the
server.js
document to import theresolvers
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 theaddPost
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 theaddPost
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 theaddPost
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 thegetPosts
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 thegetPosts
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 executingvue ui
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ vue ui
� Starting GUI...
� Ready on http://localhost:8001
- Put
client
forProject folder
,npm
forPackage manager
, unselectInitilize git repository (recommender)
forGit repository
and click onNext
- Select
Manual
forSelect a preset
and click onNext
- Select
Router
,Vuex
, unselectLinter / Formatter
and click onNext
- Select
Use history moder for Router
and click onCreate Project
- Click on
Continue without saving
Juan.Pablo.Perez@RIMDUB-0232 MINGW64 /c/Work/Training/Pre/VueJs/full-stack-vue-with-graphql-the-ultimate-guide (master)
$ vue ui
� Starting GUI...
� Ready on http://localhost:8001
✨ Creating project in C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client.
⚓ Running completion hooks...
� Generating README.md...
INFO
Vue CLI v3.8.4
15/06/2019, 16:02:34
🌠 Ready on http://localhost:8001
15/06/2019, 16:02:36
INFO
⚙ Installing CLI plugins. This might take a while...
15/06/2019, 16:41:27
INFO
> yorkie@2.0.0 install C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client\node_modules\yorkie
> node bin/install.js
15/06/2019, 16:44:56
INFO
setting up Git hooks
15/06/2019, 16:44:56
INFO
can't find .git directory, skipping Git hooks installation
15/06/2019, 16:44:56
INFO
> core-js@2.6.9 postinstall C:\Work\Training\Pre\VueJs\full-stack-vue-with-graphql-the-ultimate-guide\client\node_modules\core-js
> node scripts/postinstall || echo "ignore"
15/06/2019, 16:44:57
INFO
added 1078 packages from 891 contributors and audited 18077 packages in 206.891s
15/06/2019, 16:45:02
INFO
found 0 vulnerabilities
15/06/2019, 16:45:02
INFO
🚀 Invoking generators...
15/06/2019, 16:45:02
INFO
📦 Installing additional dependencies...
15/06/2019, 16:45:04
INFO
added 4 packages from 1 contributor and audited 18083 packages in 30.472s
15/06/2019, 16:45:38
INFO
found 0 vulnerabilities
15/06/2019, 16:45:38
INFO
🎉 Successfully created project client.
15/06/2019, 16:45:39
19. Adding Plugins with Vue GUI and Concurrently Dev Script 4min
- If we click on
plugins
we can see the plugins already installed.
- If we click on
+Add plugin
we can see a list of availableplugins
- 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 theclient
script fromcd client && npm start
tocd client && npm run serve
because it is how the script is called on theclient 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
, bothserver
andclient
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
toclient\src\components\Home.vue
and then remove theclient\src\views
folder. We are also going to remove thesrc\assets
folder and thesrc\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 usingVue 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 can use the VUETIFYTHEME GENERATOR tool to generate a
theme
for our application.
- We are going to modify the
client\src\plugins\vuetify.js
document to add thetheme
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 newtheme
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 usingVuetify
client\src\App.vue
<template>
<v-app>
<!-- Horizontal Navbar -->
<v-toolbar
fixed
color="primary"
dark
>
<!-- App Title -->
<v-toolbar-side-icon></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link
to="/"
tag="span"
style="cursor: pointer"
>
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field
flex
prepend-icon="search"
placeholder="Search posts"
color="accent"
single-line
hide-details
></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in horizontalNavItems"
:key="item.title"
:to="item.link"
>
<v-icon
class="hidden-sm-only"
left
>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<router-view />
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
}
};
</script>
- We are also going to modify the
client\src\components\Home.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 aSide Navbar
p usingVuetify
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<router-view/>
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
},
sideNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
- We are going to check if it works properly.
25. Add Routing and Page Transitions 9min
- We are going to create the
client\src\components\Auth
folder with theProfile.vue
,Signin.vue
andSignup.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 theAddPost.vue
andPosts.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 newcomponents
client\router.js
import Vue from "vue";
import Router from "vue-router";
import Home from "./components/Home.vue";
import AddPost from './components/Posts/AddPost.vue'
import Posts from './components/Posts/Posts.vue'
import Profile from './components/Auth/Profile.vue'
import Signin from './components/Auth/Signin.vue'
import Signup from './components/Auth/Signup.vue'
Vue.use(Router);
export default new Router({
mode: "history",
// base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "home",
component: Home
},
{
path: "/posts",
name: "Posts",
component: Posts
},
{
path: "/post/add",
name: "AddPost",
component: AddPost
},
{
path: "/profile",
name: "Profile",
component: Profile
},
{
path: "/signin",
name: "Signin",
component: Signin
},
{
path: "/Signup",
name: "Signup",
component: Signup
}
]
});
- We are going to modify the
client\src\App.vue
document to addPage Transitions
client\src\App.vue
<template>
<v-app style="background: #E3E3EE">
<!-- Side Navbar -->
<v-navigation-drawer app temporary fixed v-model="sideNav">
<v-toolbar color="accent" dark flat>
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<router-link to="/" tag="span" style="cursor: pointer">
<h1 class="title pl-3">VueShare</h1>
</router-link>
</v-toolbar>
<v-divider></v-divider>
<!-- Side Navbar Links -->
<v-list>
<v-list-tile ripple v-for="item in sideNavItems" :key="item.title" :to="item.link">
<v-list-tile-action>
<v-icon>{{item.icon}}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
{{item.title}}
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<!-- Horizontal Navbar -->
<v-toolbar fixed color="primary" dark>
<!-- App Title -->
<v-toolbar-side-icon @click="toggleSideNav"></v-toolbar-side-icon>
<v-toolbar-title class="hidden-xs-only">
<router-link to="/" tag="span" style="cursor: pointer">
VueShare
</router-link>
</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Search Input -->
<v-text-field flex prepend-icon="search" placeholder="Search posts" color="accent" single-line hide-details></v-text-field>
<v-spacer></v-spacer>
<!-- Horizontal Navbar Links -->
<v-toolbar-items class="hidden-xs-only">
<v-btn flat v-for="item in horizontalNavItems" :key="item.title" :to="item.link">
<v-icon class="hidden-sm-only" left>{{item.icon}}</v-icon>
{{item.title}}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<!-- App Content -->
<main>
<v-container class="mt-4">
<transition name="fade">
<router-view/>
</transition>
</v-container>
</main>
</v-app>
</template>
<script>
export default {
name: "App",
data() {
return {
sideNav: false
};
},
computed: {
horizontalNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
},
sideNavItems() {
return [
{ icon: "chat", title: "Posts", link: "/posts" },
{ icon: "lock_open", title: "Sign In", link: "/signin" },
{ icon: "create", title: "Sign Up", link: "/signup" }
];
}
},
methods: {
toggleSideNav() {
this.sideNav = !this.sideNav;
}
}
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition-property: opacity;
transition-duration: 0.25s;
}
.fade-enter-active {
transition-delay: 0.25s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
</style>
- We can now test if it works.
Section 6: Using Vue Apollo 0 / 4|24min
26. Setting up Apollo Client / Vue Apollo, Firing getPosts Query from Client 8min
- We need to install the
apollo-boost
and thevue-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 theposts
client/components/Home.vue
<template>
<v-container>
<h1>Home</h1>
<ul v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
}
}
};
</script>
- We need to modify the
typeDefs.gpl
document to include theId
field for thePost
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 aloading...
test show whenApollo
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 ofApollo
to get information from the response and store it in aVue.JS
property. There are some other methods likenetworkStatus
.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<div v-if="$apollo.loading">loading...</div>
<ul v-else v-for="post in getPosts" :key="post._id">
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
posts: []
}
},
apollo: {
getPosts: {
query: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`,
result({ data, loading, networkStatus }) {
if (!loading) {
this.posts = data.getPosts;
console.log('[networkStatus]', networkStatus)
}
}
}
}
};
</script>
- We can see all the arguments of the
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 theGraphQL
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>
- We can get more information in Apollo and GraphQL for Vue.js
28. Executing Queries with the ApolloQuery Component 6min
- We have another way of executing Queries and it is by using the
ApolloQuer
y` Component.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, data} }">
<div v-if="loading">loading...</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>
- We can even manage the errors.
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, error, data } }">
<div v-if="loading">loading...</div>
<div v-else-if="error">Error! {{error.message}}</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPost {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>
- We can also obtain the
Network Status
\client\src\components\Home.vue
<template>
<v-container>
<h1>Home</h1>
<ApolloQuery :query="getPostsQuery">
<template slot-scope="{ result: { loading, error, data, networkStatus } }">
<div v-if="loading">loading...</div>
<div v-else-if="error">Error! {{error.message}}</div>
<div v-else-if="networkStatus">Network Status: {{networkStatus}}</div>
<ul
v-else
v-for="post in data.getPosts"
:key="post._id"
>
<li>
{{post.title}} {{post.imageUrl}} {{post.description}}
</li>
<li>{{post.likes}}</li>
</ul>
</template>
</ApolloQuery>
</v-container>
</template>
<script>
import { gql } from "apollo-boost";
export default {
name: "home",
data() {
return {
getPostsQuery: gql`
query {
getPosts {
_id
title
imageUrl
description
likes
}
}
`
};
}
};
</script>
29. Add Carousel Component to Home Page 3min
- We are going to use the
Corousel 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, themain.js
main document and thestore.js
store document to use the Vuex store to call theGraphQL 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 thestore.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 thestore.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 newqueries.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
andsignupUser
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 theclient
project to add the newsignin
andsignup
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 properSignin
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 thetoken
automatically to our GraphQLqueries
andmutations
.
\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 theserver.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 thetypeDefs.gql
andresolvers.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 theuser
state, thesetUser
mutation, thegetCurrentUser
action and theuser
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 theGET_CURRENT_USER
userQUERY
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 thegetCurrentUser
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 theuser
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 theNavbar 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 amutation
and anaction
to be able tosign 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 tosign 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 newAuthGuard.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 theerror
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 theclient\src\components
and theFormAlert.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 newFormAlert
component and the newError
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 aVuetify
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 theVuetify
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 aAuthSnackbar
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 newAuthError
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 theAuthError
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 anotherAuthSnackbar
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 newsignupUser
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 theSignup
.
client\src\components\Auth\Signup.vue
<template>
<v-container
text-xs-center
mt-5
pt-5
>
<!-- Signup Title -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<h1>Get Started Here</h1>
</v-flex>
</v-layout>
<!-- Error Alert -->
<v-layout
v-if="error"
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<form-alert :message="error.message"></form-alert>
</v-flex>
</v-layout>
<!-- Signup Form -->
<v-layout
row
wrap
>
<v-flex
xs12
sm6
offset-sm3
>
<v-card
color="accent"
dark
>
<v-container>
<v-form
v-model="isFormValid"
lazy-validation
ref="form"
@submit.prevent="handleSignupUser"
>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="usernameRules"
v-model="username"
prepend-icon="face"
label="Username"
type="text"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="emailRules"
v-model="email"
prepend-icon="email"
label="Email"
type="email"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="passwordRules"
v-model="password"
prepend-icon="extension"
label="Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-text-field
:rules="passwordRules"
v-model="passwordConfirmation"
prepend-icon="gavel"
label="Confirm Password"
type="password"
required
></v-text-field>
</v-flex>
</v-layout>
<v-layout row>
<v-flex xs12>
<v-btn
:loading="loading"
:disabled="!isFormValid || loading"
color="info"
type="submit"
>
<span
slot="loader"
class="custom-loader"
>
<v-icon light>cached</v-icon>
</span>
Signup</v-btn>
<h3>Already have an account?
<router-link to="/signin">Signin</router-link>
</h3>
</v-flex>
</v-layout>
</v-form>
</v-container>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Signup",
data() {
return {
isFormValid: true,
username: "",
email: "",
password: "",
passwordConfirmation: "",
usernameRules: [
username => !!username || "Username is required",
username =>
username.length < 10 || "Username cannot be more than 10 characters"
],
emailRules: [
email => !!email || "Email is required",
email => /.@+./.test(email) || "Email must be valid"
],
passwordRules: [
password => !!password || "Password is required",
password =>
password.length >= 4 || "Password must be at least 4 characters",
confirmation => confirmation === this.password || "Passwords must match"
]
};
},
watch: {
user(value) {
// if user value changes, redirect to home page
if (value) {
this.$router.push("/");
}
}
},
computed: {
...mapGetters(["loading", "error", "user"])
},
methods: {
handleSignupUser() {
if (this.$refs.form.validate()) {
this.$store.dispatch("signupUser", {
username: this.username,
email: this.email,
password: this.password
});
}
}
}
};
</script>
<style>
.custom-loader {
animation: loader 1s infinite;
display: flex;
}
@-moz-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@-o-keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@keyframes loader {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>
- We need to ensure it works properly.
Section 10: Add Post / Infinite Scroll Components 0 / 6|58min
52. Make Add Post Form 8min
- We are going to modify the
posts\add.vue
page document to include the form that it's going to allow us to create a newpost
.
client\pages\posts\add.vue
<template>
<v-container text-xs-center mt-5 pt-5>
<!-- Add Post Title -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<h1 class="primary--text">Add Post</h1>
</v-flex>
</v-layout>
<!-- Add Post Form -->
<v-layout row wrap>
<v-flex xs12 sm6 offset-sm3>
<v-form v-model="isFormValid" lazy-validation ref="form" @submit.prevent="handleAddPost">
<!-- Title Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="titleRules" v-model="title" label="Post Title" type="text" required></v-text-field>
</v-flex>
</v-layout>
<!-- Image Url Input -->
<v-layout row>
<v-flex xs12>
<v-text-field :rules="imageRules" v-model="imageUrl" label="Image URL"