27
loading...
This website collects cookies to deliver better user experience
Figure 1
Figure 2
Figure 3
Figure 4
Figure 5
Figure 6
Figure 7
Figure 8
Figure 9
Course
. Once you enter the display name, the API ID
and Plural API ID
fields update automatically. Thus, you can leave them as they are. Lastly, enter a description of your model.Course
model.Figure 10
Course
.Author
. Go to the Schema
page and then click on the + Add button on the left-hand side. See figure 11 below for reference.Figure 11
Author
model as you created the Course
model.Author
model.Figure 12
Figure 13
Course
model will have the following fields:Figure 14
name
field cannot be empty. See figure 15 for reference.Figure 15
name
field, click on the "Create" button. After you create the field, you should see it in your dashboard.name
field, let's choose the "Markdown" field.Figure 16
Figure 17
Vote
field is of type number. Thus, go to the right-hand side and click on the "Number" field.Figure 18
Figure 19
Figure 20
Figure 21
Figure 22
Author
model.Single line text
for the full nameMulti line text
for the short biographySingle line text
for the Twitter profileFigure 23
Figure 24
Figure 25
100
and 500
words.Figure 26
Figure 27
Figure 28
Author
model as well! If you look at your dashboard, you should see the fields you created. Figure 29, below, illustrates that.Figure 29
Course
and Author
models. To recap, a course can have multiple authors, and authors can have multiple courses.Reference
field from any of the models. The Reference
field allows you to create different types of relationships between models.Figure 30
reference
and reverse
fields. They are pre-configured for you, but you can use custom options if you want. For this tutorial, the pre-configured details are enough.Figure 31
Author
from the column.Figure 32
Figure 33
Figure 34
Figure 35
Figure 36
Figure 37
Figure 38
query Courses {
courses {
name
description
publishedAt
url
vote
authors {
name
biography
twitterProfile
}
}
}
query Authors {
authors {
name
biography
twitterProfile
courses {
name
url
vote
description
}
}
}
mutation CourseCreation {
createCourse(data: {name: "Test Course", description: "Add a longer description", vote: 10, url: "https://google.com"}) {
id
name
}
}
deleteCourse
. The mutation below shows how you can achieve that.mutation CourseDeletion {
deleteCourse(where: {id: "ckqg8wi7s13ko0c56z71xw139"}) {
id
name
}
}
mutation AuthorCreation {
createAuthor(data: {name: "Test Author", biography: "A short biography.", twitterProfile: "{% twitter testauthor"}) %} {
id
name
}
}
Figure 39
Figure 40
Figure 41
Published
.Figure 42
Figure 43
Figure 44
Figure 45
Figure 46
API Access
option from the left-hand sidebar, as highlighted in figure 47, below.Content API
highlighted in figure 47, below.Figure 47
Permanent Auth Tokens
. Once you see it, click on the token to copy it, as shown in figure 48 below.Figure 48
endpoint
and auth token
, you can use the GraphCMS application from outside - for example, from a JavaScript framework such as Nuxt.npx create-nuxt-app voting-app
Figure 49
cd voting-app
components
folder, you should see two components:CourseCard.vue
component to render each course.Figure 50
dotenv
as follows:npm i @nuxtjs/dotenv
nuxt.config.js
and add the module to the buildModules
array. Your build modules should look as follows:buildModules: [
// https://go.nuxtjs.dev/eslint
'@nuxtjs/eslint-module',
// https://go.nuxtjs.dev/tailwindcss
'@nuxtjs/tailwindcss',
'@nuxtjs/dotenv'
],
.env
file, where you will add your sensitive information. Create the .env
file in the root directory of the project. You can do it as follows:touch .env
GRAPHCMS_ENDPOINT=
BEARER=
.env
file to .gitignore
so you do not make your sensitive information public.process.env.GRAPHCMS_ENDPOINT
process.env.BEARER
plugins
in the root directory of the project. After that, create a file called graphcms.js
inside the plugins
folder.graphcms.js
and add the following code:import { GraphQLClient } from 'graphql-request';
const graphcmsClient = new GraphQLClient(process.env.GRAPHCMS_ENDPOINT, {
headers: {
authorization: `Bearer ${process.env.BEARER}`
}
});
export default(_, inject) => {
inject('graphcms', graphcmsClient);
};
GraphCMS
endpoint and the bearer token. Thus, you can use the graphcms
client everywhere in your Nuxt.js application.Hero.vue
. This component only has two lines of text. Open it and add the following piece of code:<template>
<div class="flex flex-col items-center text-center mt-10">
<h1 class="text-3xl">Vote <span class="font-bold">your favourite</span> tech courses</h1>
<div class="m-4 text-lg">
<p>Finding the right courses in tech is difficult so we make it easier</p>
</div>
</div>
</template>
Figure 51
pages > index.vue
and delete everything that's inside the file. Then, add the following piece of code:<template>
<div class="relative flex flex-col items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
<Hero/>
</div>
</template>
{{ }}
- it's the data coming from the database.<template>
<div class="max-w-4xl mx-auto px-8 sm:px-6 lg:px-8 flex flex-row justify-center">
<div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
<h2 class="text-2xl leading-7 font-semibold">
<NuxtLink :to='id' class="hover:underline">{{ name }}</NuxtLink>
</h2>
<p class="mt-1 font-extralight italic text-gray-600">by {{ authors.toString() }}</p>
<p class="mt-3 text-gray-600 italic">
{{ excerpt }} <br>
</p>
<div class="flex flex-col items-center border-t border-dashed mt-5">
<p class="mt-4 pt-4 text-gray-800 font-bold text-xl tracking-wider">
The course has <code class="bg-gray-100 text-2xl p-1 rounded border">{{ newVote }}</code> votes.
</p>
<p>
<button class="bg-white hover:bg-gray-200 mt-5 mb-2 text-gray-800 font-semibold py-2 px-4 border border-gray-400 rounded shadow" @click="fetchData">Vote +1</button>
</p>
</div>
</div>
</div>
</template>
fetchData
allows users to upvote courses. When the user clicks on the voting button, the fetchData
method is called, and it makes a POST request to the database. Then, it increments the number of votes and returns the new value./upvote/${this.id}
is a custom API endpoint created in Nuxt. In the next section, you will see how to do it.newVote
. You assign the value from the vote
prop to this new property. Also, the property newVote
is updated and displayed on the page when a user votes the course.excerpt
that only shows part of the description.<script>
export default {
props: {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
description: {
type: String,
required: true
},
url: {
type: String,
required: true
},
vote: {
type: Number,
required: true
},
authors: {
type: Array,
required: true
}
},
data() {
return {
newVote: this.vote
}
},
methods: {
async fetchData() {
const options = {
method: "POST"
};
const upvoted = await fetch(`/upvote/${this.id}`, options).then(res => res.json());
this.newVote = upvoted.votes;
}
},
computed: {
excerpt() {
return this.description.substring(0, 150) + "...";
}
}
}
</script>
Figure 52
serverMiddleware
property that allows you to use additional custom API routes without needing an external server. If you want to read more about the serverMiddlware
property, I recommend the official documentation.server-middleware
. Then, create a new file, upvoteCourse.js
inside the directory.nuxt.config.js
and add the following line at the end of the file:serverMiddleware: ['~/server-middleware/upvoteCourse.js']
nuxt.config.js
should look as follows:export default {
// Target: https://go.nuxtjs.dev/config-target
target: 'static',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'Vote Tech Courses',
htmlAttrs: {
lang: 'en'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
'~/plugins/graphcms.js'
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/eslint
'@nuxtjs/eslint-module',
// https://go.nuxtjs.dev/tailwindcss
'@nuxtjs/tailwindcss',
],
serverMiddleware: ['~/server-middleware/upvoteCourse.js']
}
upvoteCourse.js
and import the following packages:const express = require('express');
const { GraphQLClient } = require('graphql-request');
express
to create the API endpoint. Secondly, you need graphql-request
to make GraphQL requests to the GraphCMS server.json()
middlewareconst app = express();
app.use(express.json());
const client = new GraphQLClient(
process.env.GRAPHCMS_ENDPOINT,
{
headers: {
authorization: `Bearer ${process.env.BEARER}`,
}
}
);
upvoteCourse.js
should look like:const express = require('express');
const { GraphQLClient } = require('graphql-request');
const app = express();
app.use(express.json());
const client = new GraphQLClient(
process.env.GRAPHCMS_ENDPOINT,
{
headers: {
authorization: `Bearer ${process.env.BEARER}`,
}
}
);
app.post('/upvote/:slug', async (req, res) => {
}
/upvote/:slug
endpoint. Thus, you need to use the POST
method. :slug
is a dynamic value, you need to retrieve it from the URL. You can get the slug from the URL as follows:app.post('/upvote/:slug', async (req, res) => {
const { slug } = req.params;
}
params
property on the request
object.slug
, which is the course IDapp.post('/upvote/:slug', async (req, res) => {
const { slug } = req.params;
const getCourse =
`
query getCourse($slug: ID!) {
course(where: { id: $slug }) {
id
vote
name
}
}
`;
const upvoteCourse =
`
mutation voteCourse($slug: ID!, $existingVotes: Int) {
updateCourse(where: { id: $slug }, data: { vote: $existingVotes }) {
id
name
vote
}
}
`;
const publishCourse =
`
mutation publishCourse($slug: ID!) {
publishCourse(where: { id: $slug }) {
id
name
vote
}
}
`;
});
const { course } = await client.request(getCourse, { slug });
const existingVotes = course.vote + 1;
slug
and existingVotes
, which is the new incremented number.const voteCourse = await client.request(upvoteCourse, { slug, existingVotes });
DRAFT
stage. That means the updated data is not visible on the frontend unless you publish it.const publishedCourse = await client.request(publishCourse, { slug });
res.json({ message: 'Course upvoted successfully!', votes: publishedCourse.publishCourse.vote });
upvoteCourse.js
should be:const express = require('express');
const { GraphQLClient } = require('graphql-request');
const app = express();
app.use(express.json());
const client = new GraphQLClient(
process.env.GRAPHCMS_ENDPOINT,
{
headers: {
authorization: `Bearer ${process.env.BEARER}`,
}
}
);
app.post('/upvote/:slug', async (req, res) => {
const { slug } = req.params;
const getCourse =
`
query getCourse($slug: ID!) {
course(where: { id: $slug }) {
id
vote
name
}
}
`;
const upvoteCourse =
`
mutation voteCourse($slug: ID!, $existingVotes: Int) {
updateCourse(where: { id: $slug }, data: { vote: $existingVotes }) {
id
name
vote
}
}
`;
const publishCourse =
`
mutation publishCourse($slug: ID!) {
publishCourse(where: { id: $slug }) {
id
name
vote
}
}
`;
const { course } = await client.request(getCourse, { slug });
const existingVotes = course.vote + 1;
const voteCourse = await client.request(upvoteCourse, { slug, existingVotes });
const publishedCourse = await client.request(publishCourse, { slug });
res.json({ message: 'Course upvoted successfully!', votes: publishedCourse.publishCourse.vote });
});
module.exports = app
CourseList.vue
component loops over the array of courses and renders each course individually.CourseCard
component for each course, and you pass the required props.<template>
<div>
<ul>
<li v-for="course in courses" :key="course.id">
<CourseCard
:id="course.id"
:name="course.name"
:description="course.description"
:url="course.url"
:vote="course.vote"
:authors="course.authors.map(author => author.name)"
/>
</li>
</ul>
</div>
</template>
courses
. It's of type array, it's required and by default, it's an empty array.<script>
export default {
props: {
courses: {
type: Array,
default: () => {
return []
},
required: true
}
},
}
</script>
CourseList
component to render courses from the database on the page.<template>
<div class="flex justify-center pt-4 space-x-2 mb-10">
<a href="https://github.com/" target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z" fill="currentColor" /></svg></a>
<a href="{% twitter " %} target="_blank"><svg
class="w-6 h-6 text-gray-600 hover:text-gray-800"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
width="32"
height="32"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
><path d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23z" fill="currentColor" /></svg></a>
</div>
</template>
pages
directory, and it's called index.js
.<template>
<div class="relative flex flex-col items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
<Hero/>
<CourseList :courses="courses" />
<Footer/>
</div>
</template>
CourseList
component, you see that the courses
prop being passed. The courses
prop is an array with objects, each object representing an individual course.courses
array from the database. You can load data from the database asynchronously using the asyncData
hook. Also, you make a GraphQL request, so you need a special package.graphql-request
package and the asyncData
hook to make a request to GraphCMS. You also specify what fields to return. Then, once you have the data from the database, you return it and pass it as a prop to CourseList
.<script>
import { gql } from 'graphql-request';
export default {
async asyncData({ $graphcms }) {
const { courses } = await $graphcms.request(
gql`
{
courses(orderBy: vote_DESC) {
id
name
description
url
vote
authors {
name
}
}
}
`
);
return { courses };
}
}
</script>
Figure 53
_slug.vue
in the' pages' folder. Since you prefixed it with the underscore, it will be a dynamic page. Then, you can access the value from the params. In this case, you can get the value through params.slug
, as you will see in the code below. {{ }}
- it's dynamic data coming from the database.<template>
<div class="relative flex flex-col items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0">
<div class="max-w-4xl mx-auto px-8 sm:px-6 lg:px-8 flex flex-col justify-center">
<Hero/>
<div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
<h2 class="text-2xl leading-7 font-semibold">
{{ course.name }}
</h2>
<p class="mt-1 font-extralight italic text-gray-600">by {{ course.authors.map(author => author.name).toString() }}</p>
<p class="mt-3 text-gray-600">
{{ course.description }} <br>
<br>
We recommend you take a look at the <a :href="course.url" target="_blank" class="text-green-500 hover:underline">course</a> page.<br>
</p>
<div class="flex flex-col items-center border-t border-dashed mt-5">
<p class="mt-4 pt-4 text-gray-800 font-bold text-xl tracking-wider">
The course has <code class="bg-gray-100 text-2xl p-1 rounded border">{{ course.vote }}</code> votes.
</p>
</div>
</div>
</div>
<div class="max-w-4xl mx-auto px-8 sm:px-6 lg:px-8 flex flex-col justify-center">
<div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6">
<h2 class="text-2xl leading-7 font-semibold mb-3">
{{ course.authors.map(author => author.name).toString() }}
</h2>
<a :href="course.authors.map(author => author.twitterProfile).toString()" class="mt-1 font-extralight italic text-gray-600"><code class="bg-gray-100 p-2 rounded border">@catalinmpit</code> Twitter</a>
<div class="flex flex-col items-center border-t border-dashed mt-5">
<p class="mt-4 pt-4 text-gray-800 tracking-wider">
{{ course.authors.map(author => author.biography).toString() }}
</p>
</div>
</div>
</div>
</div>
</template>
graphql-request
to make a GraphQL request to the GraphCMS database. You also pass the slug
to the request, which represents the ID of the course.<script>
import { gql } from 'graphql-request';
export default {
async asyncData({ $graphcms, params }) {
const { slug } = params;
const query =
gql`
query getCourse($slug: ID!) {
course(where: { id: $slug }) {
id
name
description
url
vote
authors {
name
biography
twitterProfile
}
}
}
`
;
const course = await $graphcms.request(query, { slug });
return course;
}
}
</script>
Figure 54
You can see the live application here. In addition, the GitHub repository is available at this link.