Creating a portfolio website from scratch with NextJS, TailwindCSS and Metricalp (2024)
Portfolio websites are essential for showcasing your work and attracting potential clients. In this post, we will create a portfolio website from scratch with NextJS 14, TailwindCSS and Metricalp. We will cover everything to setup website and how can you use Metricalp to increase your conversions.
Introduction
In this post, we are going to create a potfolio website with NextJS, TailwindCSS and Metricalp. We will also cover how to track user behaviours and how to make data-driven decisions with Metricalp which can boost your personal growth. The website will have two pages. One homepage and one projects page. People can leave their emails on homepage which we can contact with them. Also, they will see and discover our recent projects in projects page. Let's start with looking final view 🚀
Okay enough to talk, let's go and build it 🚀
Setup Project
First, we need to create a new NextJS project:
npx create-next-app@latest
Thanks to NextJS contributors and team, they have a great CLI installer. It will ask to you all your preferences and gonna setup project.
Here all settings I picked while I am bootstrapping this project:
We selected TailwindCSS and TypeScript, the CLI will install all dependencies and setup the project for us.
We will install Metricalp additionally to finish installation process:
npm install metricalp
We will create some folders/files. I want to share final look of project structure which we will explain every each of file below.
We said that we have a homepage with email submit form, and a projects page. Lets say we want to collect two things within Metricalp, submit_email which will be triggered everytime when a visitor clicks submit button in homepage. view_project_details which will be triggered everytime when a visitor clicks detail button of a project in projects screen. These metrics can help us about which project is getting more attention by visitors. Or even better, we can see that which visitors from which country/browser is having more attention to which project. It can definitely affect our marketing strategy. Lets make this more crazy. This is just a basic portfolio website so we don't have a database/backend etc. Then how will we collect emails? Oh, wait. We are collecting submit_email events and in Metricalp you can attach custom props to events, then what if we attach filled text input (email) to submit_email events. Then we will be using Metricalp not just as an Analytics provider also a basic database? Wow it is awesome. Yeah, we know. This is beauty of Metricalp, there are millions usage scenarios.
Okay, let's see some codes. Let's have greatest, simplistic portfolio and reach our potential customers.
Lets create an utils folder inside src folder. There will be two files inside of this folder. One is metricalp-events.ts and the other is constants.ts
src/utils/metricalp-events.ts file:
export const MetricalpEvents = {
SubmitEmail: 'submit_email',
ViewProjectDetails: 'view_project_details',
};
src/utils/constants.ts file:
export const METRICALP_TID = 'mam48'; // Replace with your Metricalp TID
Now, we are going to setup Metricalp and create our main layout. Edit the file within path and name src/app/layout.tsx:
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { MetricalpReactProvider } from '@metricalp/react';
import Link from 'next/link';
import { METRICALP_TID } from '@/utils/constants';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Melanie | Full-stack Developer',
description:
'I am Melanie, a full-stack developer with a passion for building beautiful and functional websites.',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<MetricalpReactProvider allowLocalhost tid={METRICALP_TID}>
<div className="h-full p-4 md:p-0 my-4">
<div className="w-full container mx-auto">
<div className="w-full flex items-center justify-between">
<a
className="flex items-center text-indigo-400 no-underline hover:no-underline font-bold text-2xl lg:text-4xl"
href="#"
>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-green-400 via-pink-500 to-purple-500">
Melanie‘s
</span>
</a>
<div className="flex w-1/2 justify-end content-center space-x-4">
<Link href="/">
<span className="bg-clip-text text-lg text-transparent bg-gradient-to-r from-green-400 via-pink-500 to-purple-500">
Home
</span>
</Link>
<Link href="/projects">
<span className="bg-clip-text text-lg text-transparent bg-gradient-to-r from-green-400 via-pink-500 to-purple-500">
Projects
</span>
</Link>
</div>
</div>
</div>
{children}
</div>
</MetricalpReactProvider>
</body>
</html>
);
}
Welli we set up Metricalp provider and we are wrapping our whole application with it. We are also using TailwindCSS classes to make our layout more beautiful. We are using Inter font from Google Fonts. We are also using NextJS metadata feature to set title and description of our website. We are also using Link component from NextJS to navigate between pages.
We have a top navbar, it has logo (as text) in the left and two navigation links to homepage and projects page in the right.
We are using latest version of NextJS so there is server components and client components. Basically, interactive components will be client components and all other will be server components. Lets continue with the most basic one. Create a file within path and name src/components/shared/Button.tsx
'use client';
import { ReactNode } from 'react';
type Props = {
content: ReactNode;
onClick: () => void;
};
export const Button = ({ content, onClick }: Props) => {
return (
<button
onClick={onClick}
className="bg-gradient-to-r from-purple-800 to-green-500 hover:from-pink-500 hover:to-green-500 text-white font-bold py-2 px-4 rounded focus:ring transform transition hover:scale-105 duration-300 ease-in-out"
type="button"
>
{content}
</button>
);
};
It is starting with 'use client' because it is taking/passing function as prop (onClick). Other than that it is a just basic button. But, to having a fancy look, we have tailwind css classes which is marketing our button a good gradient view. Lastly we are taking content as ReactNode that means it can be a simple text or a React Component so this makes our button more customizable. This is our basic button component so it is in shared folder. Other places will use this button with customization.
Okay, let's create mail submit form which will place in our homepage. Create a file within path and name src/components/route-specific/homepage/MailSubmitForm.tsx:
'use client';
import { Button } from '@/components/shared/Button';
import { MetricalpEvents } from '@/utils/metricalp-events';
import { metricalpEvent } from '@metricalp/react';
import { useState } from 'react';
export const MailSubmitForm = () => {
const [email, setEmail] = useState('');
return (
<form className="bg-gray-900 opacity-75 w-full shadow-lg rounded-lg px-8 pt-6 pb-8 mb-4">
<div className="mb-4">
<label
className="block text-blue-300 py-2 font-bold mb-2"
htmlFor="emailaddress"
>
Leave your email to contact
</label>
<input
className="shadow appearance-none border rounded w-full p-3 text-gray-700 leading-tight focus:ring transform transition hover:scale-105 duration-300 ease-in-out"
id="emailaddress"
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
/>
</div>
<div className="flex items-center justify-between pt-4">
<Button
content="Submit"
onClick={() => {
metricalpEvent({ type: MetricalpEvents.SubmitEmail, email });
alert('Submitted');
}}
/>
</div>
</form>
);
};
Again, it is a client component because it is interactive. Basically there is an input field, and a submit button (our Button component). When user clicks submit, we are triggering a Metricalp event, Metricalp.SubmitEmail we also passing email as prop to this event.
We kept typed text in state and we applied style with TailwindCSS classes again.
Now lets see our last client component, src/components/route-specific/projects/ProjectDetailsButton.tsx:
'use client';
import { Button } from '@/components/shared/Button';
import { MetricalpEvents } from '@/utils/metricalp-events';
import { metricalpEvent } from '@metricalp/react';
type Props = {
projectTitle: string;
};
export const ProjectDetailsButton = ({ projectTitle }: Props) => {
return (
<Button
content="Details"
onClick={() => {
metricalpEvent({
type: MetricalpEvents.ViewProjectDetails,
projectTitle,
});
alert('Details');
}}
/>
);
};
So, whenever a visitor clicks detail button of a project, we are triggering Metricalp.ViewProjectDetails event and passing projectTitle as prop. This is a basic example but you can pass more props to this event.
Okay, now we are going to develop the homepage which will not be hard, all functional components already ready
Edit the file within path and name src/app/page.tsx:
import { MailSubmitForm } from '@/components/route-specific/homepage/MailSubmitForm';
import Image from 'next/image';
export default function Home() {
return (
<div className="container pt-24 md:pt-36 mx-auto flex flex-wrap flex-col md:flex-row items-center">
<div className="flex flex-col w-full xl:w-2/5 justify-center lg:items-start overflow-y-hidden">
<h1 className="my-4 text-3xl md:text-5xl text-white opacity-75 font-bold leading-tight text-center md:text-left">
Hey, Let's{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-green-400 via-pink-500 to-purple-500">
change world
</span>{' '}
together
</h1>
<p className="leading-normal text-base md:text-2xl mb-8 text-center md:text-left">
I am Melanie, a full-stack developer with a passion for building
beautiful and functional websites.
</p>
<MailSubmitForm />
</div>
<div className="w-full xl:w-3/5 p-12 overflow-hidden">
<Image
className="mx-auto w-full md:w-4/5 transform -rotate-6 transition hover:scale-105 duration-700 ease-in-out hover:rotate-6"
src="/cover.png"
alt="Cover image"
width={1080}
height={1080}
/>
</div>
</div>
);
}
We have our main headline text, cover image and mail submit form. This is a server component so we didn't added 'use client' to top. We again applied TailwindCSS classes to have a good looking.
Now, there is only one page to remaining. Projects page. Lets create it. Create the file within path and name src/app/projects/page.tsx:
import { ProjectDetailsButton } from '@/components/route-specific/projects/ProjectDetailsButton';
import Image from 'next/image';
const projects = [
{
title: 'ToDo App',
description: 'A simple todo app built with React Native',
image: 'https://cdn.metricalp.com/web/assets/images/rn-app-example-1.png',
},
{
title: 'Metricalp Website',
description: 'The marketing website for Metricalp',
image: 'https://cdn.metricalp.com/web/assets/images/hero-v4-light.webp',
},
];
export default function Projects() {
return (
<div className="p-8 flex justify-center flex-col items-center space-y-8">
{projects.map((project) => (
<div
key={project.title}
className="block rounded-lg bg-white shadow-secondary-1 text-black"
>
<div className="relative overflow-hidden bg-cover bg-no-repeat max-h-96">
<Image
className="rounded-t-lg"
src={project.image}
width={800}
height={400}
alt={project.title}
/>
</div>
<div className="p-6 text-surface ">
<h5 className="mb-2 text-xl font-medium leading-tight">
{project.title}
</h5>
<p className="mb-4 text-base">{project.description}</p>
<ProjectDetailsButton projectTitle={project.title} />
</div>
</div>
))}
</div>
);
}
We have projects array which contains our projects. We are mapping this array and creating a card for each project. Each card has an image, title, description and a detail button. When user clicks detail button, we are triggering Metricalp.ViewProjectDetails event and passing project title as prop.
Simple right? We created a good looking, robust, fully SEO optimized portfolio website with NextJS, TailwindCSS and Metricalp. We also added Metricalp events to track user behaviours and make data-driven decisions. We also used Metricalp as a basic database to collect emails. This is just a basic example, you can do more with Metricalp. You can track every user behaviour, you can collect every data you want. You can make your website more personalized and more user-friendly. You can increase your conversions. You can reach your potential customers. You can change the world. 🚀
Metricalp is going to listen for visit events, how many unique visitors visited from which countries etc automatically. We also added some custom events additionally like submit_email and view_project_details.
But for attached custom props, we used aliases like email and projectTitle. We need to configure this in Metricalp dashboard inside tracker settings - general tab. Let's do it as final step.
Well, we set email as custom prop 1 alias to submit_email event and projectTitle as custom prop 1 alias to view_project_details event. Now, we can safely trigger and view these custom props in Metricalp dashboard.
Well, we are collecting that how many people visited our portfolio. Where are they from, what is their operating system, browser, device (mobile or desktop) etc. But additionally to these standard analytic data, we are collecting that how many unqiue visitors left their email to us. But even better we are also collecting their emails here. We do not even need a separate database. Crazy and awesome. Also, we are collecting all project detail view events. We can see that which project is getting more attention. We can see that which project is getting more attention from which country, browser, device etc. This can give shape our future career, marketing strategy etc. This is the power of Metricalp 🚀 We are proud with our product to helping achieve these in easy way.
But hey, you read the whole article, we also proud with you. I hope you gonna have greatest portfolio website and reach your potential customers. If you have any question, feedback or anything else, please do not hesitate to contact with us. We are always here to help you. 💜 🤝