Crafting my Portfolio - Projects
Hardeep Kumar
Posted on January 8, 2023
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
Next, I'll add @nuxt/content
to the modules section of nuxt.config.ts
....
modules: ['@nuxtjs/tailwindcss', '@nuxtjs/google-fonts', '@nuxt/content'],
content: {},
....
Create a new folder named content
in project root.
mkdir content
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
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
....
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.
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.
{
"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.
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.
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. 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>
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;
}
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>
Results
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
Posted on January 8, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.