DEV Community

Cover image for Crafting my Portfolio - Projects
Hardeep Kumar
Hardeep Kumar

Posted on • Updated on

Crafting my Portfolio - Projects

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 like strapi 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 free tier gone and since Railway'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 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. 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 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 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).

TLDR

I'm installing 2 more packages, named content and Image, 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.

Content Setup

I'll now follow the Installation guide for adding Content in Existing Project.

First add the package.

yarn add --dev @nuxt/content
Enter fullscreen mode Exit fullscreen mode

Next, I'll add @nuxt/content to the modules section of nuxt.config.ts

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

content: {},
....
Enter fullscreen mode Exit fullscreen mode

Create a new folder named content in project root.

mkdir content
Enter fullscreen mode Exit fullscreen mode

Image Setup

I'll simply follow the installation instructions from here.

Note: Image for Nuxt 3 is still in experimenal state.

Install the package.

yarn add --dev @nuxt/image-edge
Enter fullscreen mode Exit fullscreen mode

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

NUXT_CLOUDINARY_CLOUD_NAME=mycloudname
Enter fullscreen mode Exit fullscreen mode
....
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/`,
  },
},
....
Enter fullscreen mode Exit fullscreen mode

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.

content
└── project
    ├── 1.<file_name>.json
    ├── 2.<file_name>.json
    ├── 3.<file_name>.json
    ├── 4.<file_name>.json
    ├── 5.<file_name>.json
Enter fullscreen mode Exit fullscreen mode

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.

{
  "title": "",
  "description": "",
  "technology": [
    {
      "name": "",
      "link": ""
    }
  ],
  "github": "",
  "live": ""
}
Enter fullscreen mode Exit fullscreen mode

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.

npx nuxi add page project/index
Enter fullscreen mode Exit fullscreen mode

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

npx nuxi add component Project/Item
Enter fullscreen mode Exit fullscreen mode

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. This is the code my project page have.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

// 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;
}
Enter fullscreen mode Exit fullscreen mode

Now to the Component. Just some basic Vue code. I'll also add in a small function to randomize the button colors for technology.

<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>
Enter fullscreen mode Exit fullscreen mode

Results

Screenshot 1

Screenshot 2

preview 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

Top comments (0)