Crafting my Portfolio - Projects
Tags: vue, nuxt, tailwindcss, portfolio
Published: 2023-01-08 17:50:28

A Portfolio is incomplete without projects. 

## Decisions Decisions
>*TLDR below*

When I started to formulate the system(?) of Projects, the first thing that came to my mind was, "How would I manage the data for my Projects System?" I could just add the static data and call it a day. Or maybe I could use a [Headless CMS](https://jamstack.org/headless-cms/) like [strapi](https://strapi.io) etc. Or I could always write an API with python. After all, I'm a Backend developer first, spinning up a fully featured API or two is a matter of few hours for me.

So I thought and thought, First I laid out my needs. "I need an ecosystem so that I can manage the data for the projects. And also I need that ecosystem to be used for Blog system a well."

Without any 2nd thought, I thought of writing my own API backend. But with [Heroku](https://www.heroku.com) free tier gone and since [Railway](https://railway.app/)'s free tier gives only 20 days of project uptime unless verified with a Credit Card (which I don't have), I backed off quickly. Next up, I thought of using a Headless CMS. I've been eyeing [Contentful](https://www.contentful.com) for a while now. And its community plan is quite good. So I thought of using it. But It was kinda overkill for something very small. Then I came back to the static way. But I don't want to have all the data layout out in various component and all.

Then I recalled about [Content](https://content.nuxtjs.org). It's a file-based Headless CMS which use files of extension `.md`, `.yml`, `.csv` and `.json` a data layer for the application. And its MDC syntax is cherry on top. So I came with a plan to use `.json` files to handle project data. Basically, I'll just create a projects section using Content, put my projects in `.json` files, use the Querying functionality of Content to fetch them and populate the Components as needed.

And for blog, I'll just use `.md` files, maybe even use MDC syntax too. Which will give me the static system (good for SEO), have control on each aspect of data and no need of external hosting. But wait, there is a little problem, Media files (images etc.). The biggest obstacle here is how to manage them? I could always just put all the images in source code, but over time, it will increase the source code size by a huge margin. So I thought of using a CDN like [Cloudinary](https://cloudinary.com) for this. But the media files are not going to get uploaded on their own to it, and then I'll need to reference the images in the Content as well. So the only option left is to manually upload images to Cloudinary and reference in the Content data wherever I need to.

To make it a bit easier, It would be great having something that will reference the media files on its own by just passing the filename, instead of passing the whole URL for the file. For this, I could use the Nuxt [Image](https://v1.image.nuxtjs.org) module that comes with support to show images from Cloudinary by just passing filename(partial path to file) and even allow further manipulation of images ([Cloudinary features](https://console.cloudinary.com/documentation/image_transformations)).

## TLDR
I'm installing 2 more packages, named [content](https://content.nuxtjs.org) and [Image](https://v1.image.nuxtjs.org), from Nuxt ecosystem. Content to manage data for my projects section and Blogs too, in future and Image to easily show images in my site from [Cloudinary](https://cloudinary.com).

## Content Setup
I'll now follow the Installation guide for adding Content in [Existing Project](https://content.nuxtjs.org/get-started#add-to-a-project).

First add the package.
```zsh
yarn add --dev @nuxt/content
```

Next, I'll add `@nuxt/content` to the modules section of `nuxt.config.ts`
```ts
....
modules: ['@nuxtjs/tailwindcss', '@nuxtjs/google-fonts', '@nuxt/content'],

content: {},
....
```

Create a new folder named `content` in project root.
```zsh
mkdir content
```

## Image Setup
I'll simply follow the installation instructions from [here](https://v1.image.nuxtjs.org/get-started).
>Note: Image for Nuxt 3 is still in experimenal state.

Install the package.
```zsh
yarn add --dev @nuxt/image-edge
```

Now add `@nuxt/image-edge` to `modules` in `nuxt.config.ts` and set `baseUrl` for Cloudinary importing `cloud name` from `.env` file.
```env
NUXT_CLOUDINARY_CLOUD_NAME=mycloudname
```

```ts
....
modules: [
  '@nuxtjs/tailwindcss',
  '@nuxtjs/google-fonts',
  '@nuxt/content',
  '@nuxt/image-edge',
],

// @nuxt/image-edge: https://v1.image.nuxtjs.org/get-started
image: {
  cloudinary: {
    baseURL: `https://res.cloudinary.com/${process.env.NUXT_CLOUDINARY_CLOUD_NAME}/image/upload/`,
  },
},
....
```

With this image setup, I only need to pass folder and filename with or without extension, since either way, quality and image format will be auto decided. e.g. `<folder>/<filename(.ext)>`

## Project Content
Inside the `content` folder, I'll create a new folder named `project` where I'll put all my project related files. I decided to use `json` for my projects because I want to style stuff my way. This is how my content directory will look like.

```zsh
content
└── project
    ├── 1.<file_name>.json
    ├── 2.<file_name>.json
    ├── 3.<file_name>.json
    ├── 4.<file_name>.json
    ├── 5.<file_name>.json
```

Noticed the numbers suffixed with `.`? That's how you tell the ordering in content. I'm going to use something like this in my `json` files for storing content for projects.

```json
{
  "title": "",
  "description": "",
  "technology": [
    {
      "name": "",
      "link": ""
    }
  ],
  "github": "",
  "live": ""
}
```

I'll use conditionals to show UI for parts that might be empty in `jdon` files. Now I'll a new page for Project. I'm using directory like structure in case in future i wanna add screenshots and stuff so that I can route them to their separate pages.

```zsh
npx nuxi add page project/index
```

Now add a component so that I can reuse and customize the way I want each item to look.

```zsh
npx nuxi add component Project/Item
```

## Project Logic
In order to display projects list, I want to get all of them, sorted in a specific order. Then I'll iterate over them and style data using my custom component. For listing data, I'm [ContentList component](https://content.nuxtjs.org/api/components/content-list). This is the code my project page have.

```vue
<script lang="ts" setup>
import type { QueryBuilderParams } from '@nuxt/content/dist/runtime/types';

const query: QueryBuilderParams = {
  path: '/project/',
  sort: [{ _id: -1, $numeric: true }],
};
</script>

<template>
  <main class="container mx-auto px-4">
    <section
      class="prose max-w-none prose-headings:mt-2 prose-headings:mb-2 prose-p:font-serif prose-p:prose-2xl"
    >
      <ContentList :query="query">
        <template v-slot="{ list }">
          <ProjectItem :projects="list" />
        </template>

        <template #not-found>
          <p>No projects found.</p>
        </template>
      </ContentList>
    </section>
  </main>
</template>

<style scoped></style>
```

Now for the component named `ProjectItem`, I'll just stylize the data passed by projects prop. But first I'll add types for the Project data. And no it's not just normal type, I'll first have to extend the `ParsedContent` type and then add my own data to it.

```ts
// ProjectData.d.ts
import type { ParsedContent } from '@nuxt/content/dist/runtime/types';

export interface TechnologyTypes {
  name: string;
  link: string;
}

export interface ProjectsDataTypes extends ParsedContent {
  technology: TechnologyTypes[];
  github: string;
  live: string;
}
```

Now to the Component. Just some basic Vue code. I'll also add in a small function to randomize the button colors for technology.
```vue
<script lang="ts" setup>
import { ProjectsDataTypes } from '../../types/ProjectData';

const props = defineProps<{
  projects: [ProjectsDataTypes];
}>();

function setRandomBtnColor() {
  const btnColors = [
    'btn-primary',
    'btn-secondary',
    'btn-accent',
    'btn-neutral',
    'btn-info',
    'btn-success',
    'btn-warning',
    'btn-error',
    'btn-base-100',
  ];
  return btnColors[Math.floor(Math.random() * btnColors.length)];
}
</script>

<template>
  <div class="flex flex-col gap-28 relative py-20">
    <!-- center line -->
    <div
      class="absolute bg-primary w-0.5 top-0 bottom-0 left-0 right-0 m-auto"
    ></div>

    <!-- card -->
    <template v-for="item in props.projects" :key="item._path">
      <div
        class="card lg:card-side bg-base-100 shadow-xl border border-primary"
      >
        <!-- rounded circle on top -->
        <div
          class="bg-primary w-4 h-4 absolute left-0 right-0 mx-auto -mt-6 rounded-full"
        ></div>

        <!-- content -->
        <div class="card-body">
          <h1>{{ item.title }}</h1>
          <p class="whitespace-pre-line" v-html="item.description"></p>

          <h2>Technology</h2>
          <template v-if="item.technology.length != 0">
            <div class="flex gap-2 flex-wrap">
              <a
                v-for="tech in item.technology"
                class="btn btn-sm"
                :class="setRandomBtnColor()"
                :href="tech.link"
                target="_blank"
              >
                {{ tech.name }}
              </a>
            </div>
          </template>
          <template v-else><p>No Technology Found</p></template>

          <div class="card-actions justify-start mt-4">
            <a
              v-if="item.github.length"
              :href="`https://github.com/${item.github}`"
              target="_blank"
            >
              <v-icon
                name="ri-github-fill"
                scale="1.2"
                class="hover:text-primary"
              />
            </a>

            <a v-if="item.live.length" :href="item.live" target="_blank">
              <v-icon
                name="ri-external-link-line"
                scale="1.2"
                class="hover:text-primary"
              />
            </a>
          </div>
        </div>
      </div>
    </template>
  </div>
</template>

<style scoped></style>
```

## Results

![Screenshot 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxx44r2ui0i56yg15bgq.png)


![Screenshot 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7n122yh74ienu7isvzbl.png)

![preview gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xnn9vld59x6xpuk4eu06.gif)

## Closing Thoughts
I think I like it. Good enough.
It's been more than a month since I last wrote!! Sheesh. I was busy with my Internship so couldnt even look at my portfolio. Let's hope I'll be able to squeeze some time out of my schedule. Anyways, coming backand looking at my site, I feel a bit cringed looking at fonts. Hmmm... might change them to my all time favourite Poppins.

---
Cover Credits: [Headway](https://unsplash.com/@headwayio)