Create an online C.V with NextJs + AWS Amplify + Tailwind CSS
Tags: aws, nextjs, graphql, tailwindcss
Published: 2021-02-01 21:39:33

>Source code can be found [here](https://github.com/Rolando-Barbella/online-dev-cv)

I decided to create all my new solo projects with [AWSAmplify](https://aws.amazon.com/amplify/) and [NextJs](https://nextjs.org/docs/getting-started), so far the most efficient way of building great full-stack serverless apps that I have come across. [Tailwind CSS](https://tailwindcss.com/) is also a nice piece of technology that I'm playing with, which fits great for this small example.

## Environment & prerequisites
Before we begin, make sure you have the following:

- Node.js v10.x or later installed
- A valid and confirmed AWS account

## Next Js

Let's set up our Next Js project by first running:

> npx create-next-app online-dev-cv

Let's go to our new repo:

> cd online-dev-cv

## Tailwind CSS

The next thing is to install and configure Tailwind CSS inside our project.

Let's run:

> npm install tailwindcss@latest postcss@latest autoprefixer@latest postcss-preset-env

After installing these two, let's do:

> npx tailwindcss init

The `npx tailwindcss init` command will create a `tailwind.config.js` file, after this, we need to create another file called `style.css`

> touch styles/styles.css

Moving further, add the following lines to the styles.css file.

```css
 @tailwind base; 
 @tailwind components; 
 @tailwind utilities;
```

When the app builds, some CSS is generated with these lines of code, for example, the `@tailwind base` is the equivalent of a Normalize.css

## Post CSS

We are almost done adding TailwindCSS, what’s left, is to create the `postcss.config.js` file, which stores the configuration for PostCSS.

Let's do:

> touch postcss.config.js

Now let's add the following to the recently created file:

```js 
module.exports = {
   plugins: ["tailwindcss", "postcss-preset-env"],
};
```

Finally, add `import '../styles/styles.css'` to _pages/_app.js_ file. We can now use Tailwind everywhere in the application!

## Installing and Initializing an AWS Amplify Project

### Installing the CLI

AWS Amplify CL

>NPM
>$ npm install -g @aws-amplify/cli

>cURL (Mac & Linux)
>curl -sL https://aws-amplify.github.io/amplify-cli/install | >bash && $SHELL

>cURL (Windows)
>curl -sL https://aws-amplify.github.io/amplify-cli/install->win -o install.cmd && install.cmd

Let's now configure the CLI with our credentials.

If you'd like to see a walkthrough of this configuration process, [Nader Dabit](https://dev.to/dabit3) has a video that show's how [here](https://www.youtube.com/watch?v=fWbM5DLh25U)

Run: $ amplify configure

```
- Specify the AWS Region: us-east-1 || us-west-2 || eu-central-1
- Specify the username of the new IAM user: your-user-name
> In the AWS Console, click Next: Permissions, Next: Tags, Next: Review, & Create User to create the new IAM user. Then return to the command line & press Enter.
- Enter the access key of the newly created user:   
? accessKeyId: (<YOUR_ACCESS_KEY_ID>)  
? secretAccessKey: (<YOUR_SECRET_ACCESS_KEY>)
- Profile Name: your-user-name
```

Let's initialise a new project by running: $ amplify init

```
? Enter a name for the project: onlinedevcv
? Enter a name for the environment: dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building: javascript
? What javascript framework are you using: react
? Source Directory Path:  .
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use: your-user-name
```
 
Next, we need to define the schema models for our database, to do this, let's run:  

> $ amplify add api

```
? Please select from one of the below-mentioned services: GraphQL
? Provide API name: CV
? Choose the default authorization type for the API: API key
? Enter a description for the API key: public
? After how many days from now the API key should expire (1-365): 365
? Do you want to configure advanced settings for the GraphQL API: No, I am done.
? Do you have an annotated GraphQL schema?: No
? Choose a schema template: One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
? Do you want to edit the schema now? Yes
```

We should now see the schema open in our text editor, which we are going to modify to look like this:

```graphql
type MainDetails @model {
  id: ID!
  name: String!
  lastName: String!
  email: AWSEmail!
  location: String
  description: String
  skills: [Skill] @connection(keyName: "bySkill", fields: ["id"])
}

type Skill @model @key(name: "bySkill", fields: ["skillID"]) {
  id: ID!
  name: String!
  skillID: ID!
  mainDetails: MainDetails @connection(fields: ["skillID"])
}

```

We now have two basic models related to each other, the next thing to do is to push these changes so AWS can know about it, for that, let's simply run: 
> $ amplify push

You will be asked some additional questions while running this process, answer to them with the following:
`
? Do you want to generate code for your newly created GraphQL API: Yes
? Choose the code generation language target: javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions: src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions: Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested]: 2
`
You should now have a graphql folder with the queries, mutations, subscriptions ( all with code examples for getting, updating, and deleting data ), and the schema files.

Let's add some details to our C.V directly through AWS AppSync by running:

> $amplify console

```
? Which site do you want to open?  
  Amplify admin UI
❯ Amplify console
```
Your AWS console page for your app should be open in the browser, click on the API tab, then on the right corner click the `View in AppSync` button:

![Screen Shot 2021-01-25 at 23.27.00](https://dev-to-uploads.s3.amazonaws.com/i/pb2v8aah2pu5x0fvqvkh.png)
 
On our left menu, let's click on the query link and add our following mutation: 

```graphql
mutation CreateMainDetails{
    createMainDetails(input: {
      name:"Joe",
      lastName: "Satriani",
      email:"jsatriani@email.com",
      location: "Lima",
      description: "I'm Jamstack developer",
    }) {
      id
      name
      lastName
      email
      location
      description
    }
}
```
![Screen Shot 2021-01-26 at 21.25.56](https://dev-to-uploads.s3.amazonaws.com/i/gpfxf81k6l80f84fbiwh.png)

This should create for us, a new object with the person's main details, including a fresh `id` that we will be using for also adding skills to this person.

Save that `id` somewhere, let's now add a new skill by placing this mutation to our AWS AppSync console: 

```graphql
mutation CreateSkill {
  createSkill(input: { name: "AWS amplify", skillID:"your-main-skill-id" }) {
      id
      name
      skillID
      mainDetails {
        id
        name
        lastName
        description
        skills {
          items{
            id
            name
          }
        }
      }
    }
  }
```
![Screen Shot 2021-01-26 at 21.39.00](https://dev-to-uploads.s3.amazonaws.com/i/ertd4a0r3wb2xkgn75dr.png)

A new skill has been added, we now have enough data to pull out our online C.V

## Configuring Next Js with AWS amplify

Let's go to the `pages/_app.js` file, import the auto-generated `aws-exports.js`  and add the `Amplify.configure` function with our config settings, the file should look like this:

```js
import '../styles/globals.css'
import '../styles/styles.css'

import Amplify from 'aws-amplify';
import config from '../aws-exports';

Amplify.configure({
  ...config, ssr: true
});

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp
```

We are now ready to code this full-stack application! Let's open our `pages/index.js` file and first import what we need:

```js
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { API } from 'aws-amplify';
import { listMainDetails } from '../graphql/queries'
```
With the API function, we can call our Graphql query, to then pass it to a React state once it is resolved. We are also going to replace the default boilerplate created by NextJs with something simpler for now

```js
export default function Home() {
  let [mainDetails, setListMainDetails] = useState(null);

  useEffect(() => {
    fetchListMainDetailss()
  },[])
  
  async function fetchListMainDetailss() {
    const mainDetails = await API.graphql({
      query: listMainDetails
    });
    const { data: { listMainDetailss } } = mainDetails;
    console.log(listMainDetailss.items[0])
    setListMainDetails(listMainDetailss.items[0]);
  }

  return (
    <div className="relative py-3 sm:max-w-xl sm:mx-auto">
      <Head>
        <title>My online C.V</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
    </div>
  )
}
```

>We can create as many C.V as we want, but we are only going to need one, that's why we did `listMainDetailss.items[0]`

Next, let's start the app: _$npm run dev_

When opening _http://localhost:3000/_, we should not see anything on the screen, but if we open the console, our data should appear there:

```js
{id: "c6db2857-36a5-4d3c-a96d-9e4dcf199c77d", name: "Joe", lastName: "Satriani", email: "jsatriani@email.com", location: "Lima", …}
```

Let's add a loading state to not break the app while waiting for our data plus some JSX with Tailwind CSS classes, our final page should look like this:

```js
import Head from 'next/head';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { API } from 'aws-amplify';
import { listMainDetails } from '../graphql/queries'

export default function Home() {
  let [mainDetails, setListMainDetails] = useState(null);
  let [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchListMainDetailss()
  },[])
  
  async function fetchListMainDetailss() {
    const mainDetails = await API.graphql({
      query: listMainDetails
    });
    setIsLoading(false);
    setListMainDetails(listMainDetailss.items[0]);
  }
  if(isLoading || !mainDetails) {
    return <p>..Loading</p>
  }
  return (
    <div className="relative py-3 sm:max-w-xl sm:mx-auto">
      <Head>
        <title>My online C.V</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="mt-5 md:mt-5 md:col-span-2">
        <div className="bg-white shadow overflow-hidden sm:rounded-lg">
          <div className="px-4 py-5 sm:px-6">
            <h3 className="text-lg leading-6 font-medium text-gray-900">
              My Online C.V
            </h3>
          </div>
          <div className="border-t border-gray-200">
            <dl>
              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <dt className="text-sm font-medium text-gray-500">
                  Name
                </dt>
                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
                  {mainDetails.name}
                </dd>
              </div>
              <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <dt className="text-sm font-medium text-gray-500">
                  Last Name
                </dt>
                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
                  {mainDetails.lastName}
                </dd>
              </div>
              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <dt className="text-sm font-medium text-gray-500">
                  Email address
                </dt>
                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
                  {mainDetails.email}
                </dd>
              </div>
              <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <dt className="text-sm font-medium text-gray-500">
                  Location
                </dt>
                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
                  {mainDetails.location}
                </dd>
              </div>
              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <dt className="text-sm font-medium text-gray-500">
                  Description
                </dt>
                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
                  {mainDetails.description}
                </dd>
               </div>
               <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                <Link href={`/edit/${mainDetails.id}`}>
                  <div className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                    Edit
                  </div>
                </Link>
              </div>
            </dl>
          </div>
        </div>
      </div>
    </div>
  )
}

```

Notice we created an edit "button" with a href tag `href={'/edit/${mainDetails.id}'}`, that means we will need a new page where will be updating our C.V based on the `mainDetails` id.

Let's create a new folder inside our `page` directory called `edit`, then we need a new file name `[id].js` inside
 
Open the new page `(page/[id].js)` copy and paste the following code:

```js
import { useState, useEffect } from 'react';
import { API } from 'aws-amplify';
import { useRouter } from 'next/router';
import { getMainDetails, listMainDetails  } from '../../graphql/queries';
import { updateMainDetails } from '../../graphql/mutations';

const UpdateDetails = ({ mainDetails }) => {
  const router = useRouter();
  if (router.isFallback || !mainDetails) {
    return <div>Loading...</div>
  }
  const [onChangeDetail, setOnChangeDetail] = useState({ 
    name: '',  
    lastName: '' , 
    email: '', 
    location: '',
    description: '',
    skills: { items: []},
  });
  
  useEffect(() => {
    setOnChangeDetail(mainDetails);
  },[]);
 
  const handleInpuChange = (event,keyName)  =>  {
    event.persist();
    setOnChangeDetail( (onChangeDetail)  =>  {
      return  { ...onChangeDetail,  [ keyName ] : event.target.value }
    });
  }
  
  const updateDetails = async () => {
    delete onChangeDetail.skills
    await API.graphql({
      query: updateMainDetails,
      variables: { input: onChangeDetail },
    })
    router.push(`/`);
  }

  return (
    <>
      <div className="shadow sm:rounded-md sm:overflow-hidden relative py-3 sm:max-w-xl sm:mx-auto mt-4">
        <div className="px-4 py-5 bg-white space-y-6 sm:p-6">
          <h2 className="text-lg">Main Details</h2>
          <div>
            <label htmlFor="name" className="block text-sm font-medium text-gray-700">
              Name
            </label>
            <div className="mt-1">
              <input 
                type="text" 
                name="name"
                value={onChangeDetail.name}
                onChange = { (event) => handleInpuChange(event ,'name') }
                className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" 
              />
            </div>
          </div>

          <div>
            <label htmlFor="last name" className="block text-sm font-medium text-gray-700">
              Last name
            </label>
            <div className="mt-1">
              <input 
                type="text" 
                name="last name" 
                value={onChangeDetail.lastName}
                onChange = { (event) => handleInpuChange(event ,'lastName') }
                className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" 
              />
            </div>
          </div>

          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <div className="mt-1">
              <input 
                type="email" 
                name="email" 
                value={onChangeDetail.email}
                onChange = { (event) => handleInpuChange(event ,'email') }
                className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" 
              />
            </div>
          </div>

          <div>
            <label htmlFor="location" className="block text-sm font-medium text-gray-700">
              Location
            </label>
            <div className="mt-1">
              <input 
                type="text" 
                name="location"
                value={onChangeDetail.location}
                onChange = { (event) => handleInpuChange(event ,'location') }
                className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" 
              />
            </div>
          </div>

          <div>
            <label htmlFor="description" className="block text-sm font-medium text-gray-700">
              Description
            </label>
            <div className="mt-1">
              <textarea 
                name="description" 
                rows="3" 
                value={onChangeDetail.description}
                onChange = { (event) => handleInpuChange(event ,'description') }
                className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" 
                placeholder="Some words about yourself"
              />
            </div>
          </div>
        </div>
      </div>
      <div className="text-center mt-1 px-4 py-5 space-y-6 sm:p-6">
        <button 
          type="submit"
          onClick={updateDetails}
          className="inline-flex justify-center py-2 px-20 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          Update C.V
        </button>
      </div>
    </>
  )
}

export async function getStaticPaths() {
  const postMainDetails = await API.graphql({
    query: listMainDetails,
  });

  const paths = postMainDetails.data.listMainDetailss.items.map(post => ({ params: { id: post.id }}))
  return {
    paths,
    fallback: true,
  }
}

export async function getStaticProps({ params }) {
  const { id } = params
  let mainDetails = await API.graphql({
    query: getMainDetails,
    variables: { id }
  });
  return {
    props: {
      mainDetails: mainDetails.data.getMainDetails
    }
  }
}

export default UpdateDetails
```
 
If you are not familiar with `Next js` notice there is a couple of interesting functions at the end of the page. 

`getStaticProps` is for enabling the `MainDetails` data into the page as props at build time.

`getStaticPaths` is for creating dynamically pages at build time based on the `MainDetails` coming back from the API

The rest is the normal React code seen in many apps plus our corresponding query and mutation to GET and POST data ( make sure the `updateDetails` mutation includes the `skills{ items }` object)

We have not forgotten about our `skills` data yet, we will take care of this one in a separate component, outside our `pages` folder let's create a new file `components/skillsUpdate` and paste the following code:

```js
import { useState, useEffect } from 'react';
import { API, graphqlOperation } from 'aws-amplify';
import { createSkill } from '../graphql/mutations';
import { onCreateSkill } from '../graphql/subscriptions';
import { v4 as uuid } from 'uuid';

const SkillsUpdate = ({ skills, id }) => {
  const [onChangeSkill, setOnChangeSkill] = useState('');
  const [newSkills, setNewSkills] = useState([]);

  useEffect(() => {
    setNewSkills(skills)
  },[])

  useEffect(() => {
    const subscription = API.graphql(graphqlOperation(onCreateSkill)).subscribe({
      next: (eventData) => {
        const newSkill = eventData.value.data.onCreateSkill;
        const id = uuid()

        setNewSkills([...skills, { id, name: newSkill.name}])
      },
      error: error => console.error(error)
    })
    return () => subscription.unsubscribe()
  }, [])
  
  const addSkill = async() => {
    await API.graphql({
      query: createSkill,
      variables: { 
        input: { skillID: id, name: onChangeSkill }
      }
    }); 
    setOnChangeSkill('');
  }
  return (
    <div className="shadow sm:rounded-md sm:overflow-hidden relative py-3 sm:max-w-xl sm:mx-auto mt-4">
      <div className="mt-1 px-4 py-5 space-y-6 sm:p-6">
        <h2 className="text-lg">Skills</h2>
        <input 
          type="text" 
          name="skill"
          value={onChangeSkill}
          onChange = { (event) => setOnChangeSkill(event.target.value) }
          className="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-md sm:text-sm border-gray-300" 
        />
      <div className="flex flex-wrap -m-2 mb-4">
        {newSkills.map(skill => (
          <div
            key={skill.id}
            className="bg-green-400 px-10 py-2 text-white text-center rounded-full mb-3 mr-2"
          >
            {skill.name}
          </div>
          ))
        }
      </div>
      <div>
        <button 
          type="submit"
          onClick={addSkill}
          className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
          Add skill
        </button>
        </div>
      </div>
    </div>
  )
}

export default SkillsUpdate;
```

Here we are tackling 3 things:

- Rendering the skills data through props coming from our _[id].js_ page

- Adding skills to our data, thanks to our Graphql mutation

- Managing subscription so we can have real updates every time we add a new skill ( make sure the `onCreateSkill` subscription includes the `skills{ items }` object) 

Finally, we need to import and place our SkillsUpdate component into our `[id].js` page (about the update button) to get all the pieces together

```js
.....
import SkillsUpdate from '../../components/skillsUpdate';
```
and

```js
.....
     <SkillsUpdate 
        skills={mainDetails.skills.items}
        id={mainDetails.id} 
      />
      <div className="text-center mt-1 px-4 py-5 space-y-6 sm:p-6">
        <button 
          type="submit"
          onClick={updateDetails}
          className="inline-flex justify-center py-2 px-20 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
        >
          Update C.V
        </button>
      </div>
```
## Deployment 
Create a new file, name it `serverless.yml`, and place the following code :

```yml
nextamplified:
  component: "@sls-next/serverless-component@1.18.0"
```

Run `npx serverless` 

After a few seconds of creating all the necessary files, you should see a message with your deployed URL

You now have two new big folders created `/.serverless` and `/.serverless_nextjs`, they both can go to the `.gitignore` file

Done, if you have any questions, feel free to ask in the comment box.

If you think other people should read this post. Tweet, share and follow me on [twitter] (https://twitter.com/rolandobarbella)