How I Built My Portfolio Site with Next.js and Tailwind CSS
Last week marked the launch of the first version of my developer portfolio, a significant milestone I’ve been looking forward to for quite some time. Over the past four years, I’ve accumulated a handful of side projects, and it felt imperative to finally give them a platform for showcase, especially since I’m looking for a new job!
For the framework, I leaned into Next.js, drawn by its robust server-side rendering capabilities. This choice was strategic, considering the impactful benefits such as enhanced network performance and notably faster load times, key for a smooth user experience.
This venture also led me to explore Tailwind CSS for the first time. Initially unfamiliar, I was intrigued by its widespread adoption and the flexibility it promised in customization. My first experience using it was enlightening; Tailwind’s approach to styling (which is more about UI customization and ease of reusable CSS rules and classes) took a few days for me to grasp as the documentation is vast. However, once I got the hang of it, Tailwind quickly became a tool I’ll keep in my developer tool belt going forward.
The process of building and launching my portfolio has been both enriching and revealing, offering a blend of tried-and-true techniques with new, cutting-edge tools. For anyone interested in building their own site with the same tools, here’s how I built mine:
Step 1: Project Setup & Configuration
After choosing my tools, I had to run all the necessary command line prompts to generate a Next.js project configured with Tailwind. I also made sure to include TypeScript for better error detection and to hold myself accountable when maintaining the project. Here’s how you can get started per the official documentation on nextjs.org:
npx create-next-app@latest
On installation, you’ll be asked the following prompts. Be sure to say ‘yes’ to the TypeScript and Tailwind questions if you want your project configured like mine — you’ll save yourself some headaches in the long-run if you become familiar with TypeScript. ESLint is a great option for linting in your project, and I did use App Router as that’s recommended by Next.js at this time — more on that later:
What is your project named? my-app //mine was 'portfolio'
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
Once installation is complete, your project structure should look similar to this:
Step 2: Tailwind Customization
One aspect of this project that took me a while to grasp is that Tailwind is not a React component library. There is a Tailwind UI library out there, but a lot of components sit behind a pay wall.
As it turns out, a lot of the cool things people build with Tailwind are incredibly customized, so if you’re looking for a quick UI solution for your project, Tailwind might not be for you. If you have no knowledge of CSS selectors and classes, I would suggest you practice a bit with a tutorial, because in my opinion, Tailwind requires some adept knowledge of CSS.
If you already have some CSS knowledge, configuring Tailwind is actually pretty simple. Out of the box in a Next.js project, your tailwind.config.ts
will look like this:
//tailwind.config.ts
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;
This file is important, as it is the place where most customization will occur for your project. I ended up removing the default background image, and I added my own color palette and fonts. Here’s what my tailwind.config.ts
file looked like after I did that:
//my tailwind.config.ts file
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
//tailwind allows devs to add their own custom color palettes
//more on how these will be used in-project later
colors: {
blue: "#347BFF",
purple: "#838CE3",
yellow: "#f9be1e",
white: "#fff",
black: "#000",
},
extend: {
fontFamily: {
//monospace is used throughout my project, for headers, dates, tags, etc.
mono: ["monospace"],
},
},
},
plugins: [],
};
export default config;
Another file of importance is globals.css
, and if you setup your project like mine, you can find that in the /app
folder in your project directory. This file is where some default Next.js styles are applied, and it’s also where Tailwind’s base styles are loaded:
/*
globals.css
*/
/*
tailwind's default styles are loaded in at the top
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
I personally didn’t want to use some of Next’s default styles, so I went ahead and commented out some of them; I didn’t want a dark mode in my project, at least for now. I also have a custom font, so I imported it at the top of the file from google fonts:
/*
my globals.css
*/
/*
google fonts import - still haven't figured out how I'm going to use it though!
*/
@import url('https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/*
The lines below necessitate dark mode, but since I'm not using that now,
I went ahead and commented it out - maybe in the future I'll implement!
*/
/* :root {
--black: rgba(0, 0, 0, 1);
--red: rgba(255, 0, 0, 1);
--white: rgba(255, 255, 255, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
} */
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
Step 3: UX/UI Considerations
I am not a UI or UX designer. But I can definitely take inspiration from someone else and generate something that looks cool. In building my portfolio, I was heavily inspired by Brittany Chiang, an incredible front-end software engineer who puts major emphasis on accessibility in her designs. I loved Brittany’s sticky navigation on the left (which is visible only on desktop), and I wanted to build my portfolio in a similar fashion:
Brittany’s site also has some accessibility considerations when viewed on mobile, including keeping page headers visible while the user scrolls through a section — I wanted that for my portfolio as well:
I also loved how Brittany listed her work experience, creating button-like labels for the skills acquired or used for each job.
I knew I wanted to build my site with my elements placed on the page in a similar fashion, so I used Brittany’s site as a mockup for how I would structure mine. I didn’t want my site to be as dark — I wanted something lighter where I could include pops of color here and there as that is more of my personal aesthetic.
Step 4: Start Writing Code
With some great inspiration in mind, I was ready to start writing code. Using Next’s App Router configuration, my project structure eventually ended up looking like this:
What’s great about the App Router is the ease of using both the layout.tsx
and page.tsx
components that come right out of the box upon initial configuration. Because my site ended up not having multiple routes or pages, I felt that this setup was the best one for my use case. If, however, you wish to have separate pages for /about
, /resume
, or other portfolio related pages, all you have to do is create a new folder titled the same as the route you wish to create, within the app
directory of your project. For an /about
route, add page.tsx
and layout.tsx
files within app/about
folder, and you’re good to go. When you go to localhost:3000/about
, the contents of the page.tsx
component will show up on your about page once you add your React code. Simple. Read this for more instructions on setting up routes using this method.
The page.tsx
component will serve as the page that renders by default when you go to localhost:3000
or wherever your site is deployed in production. My page.tsx
is structured like this:
export default function Home(): JSX.Element {
return (
<div className="lg:flex lg:justify-between lg:gap-4">
<header className="lg:sticky lg:top-0 lg:flex lg:max-h-screen lg:w-1/2 lg:flex-col lg:justify-between lg:py-24 xl:px-10">
<div>
<MainHeader />
<Navigation />
</div>
<div>
<SocialLinks />
</div>
</header>
<main id="content" className="pt-24 lg:w-1/2 lg:py-24">
<AboutSection />
<ExperienceSection />
<ProjectsSection/>
<BlogSection />
<FooterSection />
</main>
</div>
);
}
And my layout.tsx
, which adds some global styling in the body
tag, looks like this:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Casey Whittaker",
description:
"Personal portfolio of work experience, projects, and blog posts",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body
className={`${inter.className} mx-auto min-h-screen max-w-screen-xl px-6 py-12 font-sans md:px-12 md:py-20 lg:px-24 lg:py-0`}
>
{children}
</body>
</html>
);
}
If you have a layout.tsx
and page.tsx
file in the same folder, layout.tsx
will act as a wrapper for that page, which makes adding things like navigation and a footer very simple to add to every page of your app. {children}
renders whatever is inside the page.tsx
component.
Looking back at my page.tsx
component, the tailwind classes for the first div
provide a flexbox for the entire page. It serves as a page wrapper that places both my header/nav and content sections. The header
element just beneath that structures the components that contain my MainHeader
, Navigation
, and SocialLinks
. The classes used in the header
allow that content to remain “sticky” on large screens, and it has the adequate margin, padding, and content alignment I preferred for my design.
Beneath that, in the main
element, I added classes to add some additional padding, and on large screens, some sizing considerations. The main
element contains all the content for my AboutSection
, ExperienceSection
, ProjectsSection
, BlogSection
, and FooterSection
. Each of those components have unique considerations — For example, I ended up pulling data from my GitHub and from this very blog.
In my page.tsx
file, I added a few useEffect
hooks to retrieve that data. The updated file looks something like this:
export default function Home(): JSX.Element {
const [posts, setPosts] = useState([]);
const [projects, setProjects] = useState([]);
const [error, setError] = useState(false);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch(
"https://api.rss2json.com/v1/api.json?rss_url=https://medium.com/feed/@cwhitt91"
);
const data = await res.json();
const items = data.items.slice(0, 3);
setPosts(items);
} catch {
setError(true);
}
}
fetchData();
}, []);
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("https://api.github.com/users/cdwhitt/repos");
const data = await res.json();
//not showing any github projects without a description
const filteredData = data.filter(
(item: { description: null }) => item.description !== null
);
setProjects(filteredData);
} catch (error) {
setError(true);
}
}
fetchData();
}, []);
return (
<div className="lg:flex lg:justify-between lg:gap-4">
<header className="lg:sticky lg:top-0 lg:flex lg:max-h-screen lg:w-1/2 lg:flex-col lg:justify-between lg:py-24 xl:px-10">
<div>
<MainHeader />
<Navigation />
</div>
<div>
<SocialLinks />
</div>
</header>
<main id="content" className="pt-24 lg:w-1/2 lg:py-24">
<AboutSection />
<ExperienceSection />
{/* Both of these components are structured to take the fetched data from above */}
<ProjectsSection projects={projects} />
<BlogSection posts={posts} />
<FooterSection />
</main>
</div>
);
}
In addition to the other imports, as Next.js uses server side rendering for performance reasons, you’ll need to specify at the top of any file you wish to use for network requests. The top of my page.tsx
file looks like this:
"use client";
import { useEffect, useState } from "react";
import { MainHeader } from "./components/header-sections/MainHeader";
import { Navigation } from "./components/header-sections/Navigation";
import { SocialLinks } from "./components/header-sections/SocialLinks";
import { AboutSection } from "./components/main-sections/AboutSection";
import { BlogSection } from "./components/main-sections/BlogSection";
import { ExperienceSection } from "./components/main-sections/ExperienceSection";
import { ProjectsSection } from "./components/main-sections/ProjectsSection";
import { FooterSection } from "./components/main-sections/FooterSection";
"use client"
informs the build that all components within this specified one will be used as part of the client bundle and can therefore make network requests. More information on "use client"
can be found here.
Step 5: React Components and Personal Styling
Now that the overall page structure is in place, let’s take a look at each of the components within page.tsx
as well as their content and how it’s displayed.
1. header-sections/MainHeader.tsx
In essence, this file is pretty simple. It just contains the page header, which is my name and title. You’ll notice I’ve used a custom color for blue that I specified earlier in my tailwind.config.ts
file — anytime I use a Tailwind class that accepts color, I can use one of my custom ones, and because I named that color blue
, Tailwind will know to use it:
export const MainHeader: React.FC = () => {
return (
<div>
<h1 className="text-4xl font-bold tracking-tight text-blue sm:text-5xl font-mono">
casey.whittaker
</h1>
<h2 className="mt-3 text-lg font-medium tracking-tight text-black sm:text-xl font-mono">
software engineer // writer
</h2>
</div>
);
};
2. header-sections/Navigation.tsx
This component contains all the in-page jump links. When a user clicks on one, they will be brought to a specific section of the page (instead of a new route, which was my personal preference). The Tailwind classes used here (specified in span1Classes
, span2Classes
, and aClasses
) allow my links to show a small transition when hovered over for a cool little effect:
import Link from "next/link";
import { navItems } from "./content";
export const Navigation: React.FC = () => {
const span1Classes = `nav-indicator mr-4 h-px w-8 transition-all group-hover:w-16 group-hover:bg-blue group-focus-visible:w-16 group-focus-visible:bg-blue`;
const span2Classes = `nav-text text-sm font-bold font-mono uppercase tracking-widest text-blue text-black group-focus-visible:text-blue`;
const aClasses = `group flex items-center py-3`;
return (
<nav className="nav hidden lg:block" aria-label="In-page jump links">
<ul className="mt-16 w-max">
{navItems.map((link) => {
return (
<li key={link.name}>
<Link className={aClasses} href={link.href}>
<span className={span1Classes} />
<span className={span2Classes}>{link.name}</span>
</Link>
</li>
);
})}
</ul>
</nav>
);
};
navItems
is just an array of items I have stored in a content.ts
file:
export const navItems: { name: string; href: string }[] = [
{ name: "about", href: "/#about" },
{ name: "experience", href: "/#experience" },
{ name: "projects", href: "/#projects" },
{ name: "blog", href: "/#blog" },
];
3. header-sections/SocialLinks
Of course I needed to link all my social accounts to my portfolio, so this is how I did that:
import { socialLinks } from "./content";
export const SocialLinks: React.FC = () => {
return (
<ul className="ml-1 mt-8 flex lg:justify-center" aria-label="Social media">
{socialLinks.map((link) => (
<li key={link.id} className="mr-5 text-xs">
<a
href={link.url}
target="_blank"
rel="noreferrer noopener"
aria-label={`${link.id} (opens in a new tab)`}
>
<span className="sr-only">{link.id}</span>
{link.icon}
</a>
</li>
))}
</ul>
);
};
…And all my social media information is stored similarly in the same content.ts
file like so:
export const socialLinks: {
id: string;
url: string;
icon: ReactElement<any, string | JSXElementConstructor<any>>;
}[] = [
{
id: "Twitter",
url: "https://www.twitter.com/theCaseyWhitt",
icon: <FaXTwitter size={25} color="#6A6A6A" />,
},
{
id: "GitHub",
url: "http://www.github.com/cdwhitt",
icon: <FaGithub size={25} color="#6A6A6A" />,
},
{
id: "LinkedIn",
url: "https://www.linkedin.com/in/casey-whittaker",
icon: <FaLinkedinIn size={25} color="#6A6A6A" />,
},
{
id: "Medium",
url: "https://www.medium.com/@cwhitt91",
icon: <FaMediumM size={25} color="#6A6A6A" />,
},
];
One quick thing to note — I decided to use the react-icons library for my social media icons. It’s great, and it’s free! You can specify size and color as well. They have many, many icons that are free to use.
4. main-sections/AboutSection.tsx
The main section components are where the bulk of my site live. My about section is pretty straightforward in terms of how I coded it, but a few key considerations are worth noting as I tried to make some styles easier to retrieve:
import {
contentLinkClasses,
divHeaderWrapperClasses,
sectionClasses,
sectionHeaderClasses,
} from "./styles";
export const AboutSection: React.FC = () => {
return (
<section id="about" className={sectionClasses} aria-label="About me">
<div className={divHeaderWrapperClasses}>
<h2 className={sectionHeaderClasses}>About</h2>
</div>
<div>
<p className="mb-4">
In the era of personal blogs and MySpace, the limitless creativity on
the internet captivated me. As a kid, I was fascinated by how websites
worked; I had no idea that people with fine-tuned skills could use
HTML and CSS to create visually striking websites, but I knew I wanted
to learn. When I was 12 years old, I began my digital foray by
launching my first blog. Despite a small readership, it was my first
real venture into web design, teaching me invaluable skills as I
experimented with HTML, CSS, and even PHP. This blog wasn't just a
youthful hobby; it laid the groundwork for my future in software
development.
</p>
<p className="mb-4">
Fast forward to today, and thanks to{" "}
<a
href="http://www.launchacademy.com"
target="_blank"
className={contentLinkClasses}
>
Launch Academy
</a>{" "}
in Boston, my programming skills have flourished. Now, I'm proudly
working as a Software Engineer, having been employed by three
different companies - two startups and one large company - in the
Boston area, turning my early passion into a thriving career.
</p>
<p className="mb-4">
When I'm not writing code, you can find me with a book in hand,
probably sitting in a park, trying to hit{" "}
<a
href="https://www.goodreads.com/user/show/104339093-casey-whittaker"
target="_blank"
className={contentLinkClasses}
>
my reading goal for the year
</a>
.
</p>
</div>
</section>
);
};
I created a styles.ts
file because many styles in the main section of my site were used over and over again. Creating a small utility file helped reduce the amount of Tailwind classes I had to write which ultimately makes my site much easier to update in the future:
export const contentLinkClasses: string =
"text-blue transition hover:bg-yellow hover:text-black font-medium ";
export const sectionClasses: string =
"mb-16 scroll-mt-16 md:mb-24 lg:mb-36 lg:scroll-mt-24";
export const divHeaderWrapperClasses: string =
"sticky top-0 z-20 -mx-6 mb-4 w-screen bg-slate-900/75 px-6 py-5 backdrop-blur md:-mx-12 md:px-12 lg:sr-only lg:relative lg:top-auto lg:mx-auto lg:w-full lg:px-0 lg:py-0 lg:opacity-0";
export const sectionHeaderClasses: string =
"text-md font-bold uppercase tracking-widest text-slate-200 lg:sr-only";
On mobile, you’ll notice that my headers (styled with both divHeaderWrapperClasses
and sectionHeaderClasses
) are only visible on a mobile sized screen, and when a user scrolls from top to bottom, those headers remain at the top of the page until the next section appears — they also have a bit of a backdrop blur for a cool little screen effect. Once again, I am very grateful to Brittany Chiang for the inspiration here.
5. main-sections/ExperienceSection.tsx
I had a ton of fun styling this section. I think the end result looks so cool, especially because I wanted to find a fun way to display the skills I gained/used at each work experience in a creative way. If you work in tech, hopefully you’ve noticed that each skill listed in the screenshot below is tied to the color associated with the branding of that technology. Here’s how that section looks on desktop, and what the code looks like for the component:
import { experienceContent, skillsColorLookup } from "./content";
import {
contentDateClasses,
contentDivClasses,
contentLinkClasses,
divHeaderWrapperClasses,
sectionClasses,
sectionHeaderClasses,
skillsClasses,
} from "./styles";
export const ExperienceSection: React.FC = () => {
return (
<section
id="experience"
className={sectionClasses}
aria-label="Work experience"
>
<div className={divHeaderWrapperClasses}>
<h2 className={sectionHeaderClasses}>Experience</h2>
</div>
<div>
<ol className="group/list">
{experienceContent.map((job) => (
<li key={job.id} className="mb-12">
<div className={contentDivClasses}>
<header
className={contentDateClasses}
aria-label={job.duration}
>
{job.duration}
</header>
<div className="z-10 sm:col-span-6">
<h3
className="font-bold"
aria-label={`${job.title} - ${job.id}`}
>
{job.title} ⚡️ {job.id}
</h3>
{job.url && (
<p>
🔗:{" "}
<a
href={job.url}
target="_blank"
className={contentLinkClasses}
>
{`${job.url.substring(0, 30)}${
job.url.length > 30 ? "..." : ""
}`}
</a>
</p>
)}
<p>{job.about}</p>
<ul
className="mt-2 flex flex-wrap"
aria-label="Technologies used"
>
{job.skills.map((skill) => (
<li key={skill} className="mr-1.5 mt-2">
<div
className={skillsClasses}
style={
Array.isArray(skillsColorLookup[skill])
? {
borderBottom: "5px",
borderStyle: "solid",
borderImage: `linear-gradient(to right, ${skillsColorLookup[skill][0]}, ${skillsColorLookup[skill][1]}) 1`,
}
: {
borderBottom: `5px solid ${skillsColorLookup[skill]}`,
}
}
>
{skill.toUpperCase()}
</div>
</li>
))}
</ul>
</div>
</div>
</li>
))}
</ol>
<p className="text-center">
<a
href="https://www.linkedin.com/in/casey-whittaker/"
target="_blank"
className={contentLinkClasses}
>
👉 Connect with me on LinkedIn
</a>
</p>
</div>
</section>
);
};
In order to create the mapping for this component, I created another separate content.ts
file specifically for job experience — to make the skills sections have that cool underline effect, I created an object utilized for color lookup:
//tech with multiple colors for branding store two colors in an array
export const skillsColorLookup: { [key: string]: string | string[] } = {
javascript: "#f9be1e",
typescript: "#3178C6",
python: ["#3D78A8", "#FAD44A"],
react: ["#139ECA", "#67DAFB"],
node: "#70C04F",
"next.js": ["#000", "#fff"],
aws: "#F89C1C",
dynamodb: "#3277BA",
postgresql: "#386696",
"ruby on rails": ["#C50201", "#fff"],
graphql: "#E63DAE",
tailwind: ["#3FBDF8", "#fff"],
mongodb: "#55AD49",
};
export const experienceContent: {
id: string;
title: string;
url?: string;
duration: string;
about: string;
skills: string[];
}[] = [
{
id: "TransitMatters",
title: "Volunteer Software Engineer",
url: "https://transitmatters.org/",
duration: "Dec 2023 - Present",
about:
"I started working with TransitMatters in December of 2023, working on bugs and new features for their data dashboard that is used by MBTA riders and news outlets.",
skills: [
"react",
"next.js",
"node",
"aws",
"javascript",
"typescript",
"tailwind",
],
},
...other experience listed below
]
In the component itself, you’ll notice that I’m just mapping over the experienceContent
array to generate <li>
elements with specific styling for each piece of content. The skills
arrays for each experience was fun to code, but it was also a bit tricky as there was no great way for Tailwind to natively style those pieces of text. For some reason unknown to me, Tailwind didn’t always recognize colors I applied to those elements, so I had to directly apply those colors in a style
prop like so:
{job.skills.map((skill) => (
<li key={skill} className="mr-1.5 mt-2">
<div
className={skillsClasses}
style={
Array.isArray(skillsColorLookup[skill])
? {
borderBottom: "5px",
borderStyle: "solid",
borderImage: `linear-gradient(to right, ${skillsColorLookup[skill][0]}, ${skillsColorLookup[skill][1]}) 1`,
}
: {
borderBottom: `5px solid ${skillsColorLookup[skill]}`,
}
}
>
{skill.toUpperCase()}
</div>
</li>
))}
The skillsColorLookup
allowed me to grab the color (or colors) associated to that specific piece of technology. In the case of multiple colors, I created a gradient underline, which I think adds a really cool effect that makes things pop nicely.
At this point, my styles.ts
has even more re-usable classes and should look like this after having built my ExperienceSection
component:
export const contentDivClasses: string =
"transition-all hover:bg-blue/20 rounded group relative grid p-2 sm:grid-cols-8 sm:gap-8 md:gap-4";
export const contentDateClasses: string =
"z-10 mb-2 mt-1 text-xs font-semibold uppercase tracking-wide sm:col-span-2 font-mono";
export const tagClasses: string = "font-mono text-black -p-1";
export const skillsClasses: string = "font-mono text-black";
export const contentLinkClasses: string =
"text-blue transition hover:bg-yellow hover:text-black font-medium ";
export const sectionClasses: string =
"mb-16 scroll-mt-16 md:mb-24 lg:mb-36 lg:scroll-mt-24";
export const divHeaderWrapperClasses: string =
"sticky top-0 z-20 -mx-6 mb-4 w-screen bg-slate-900/75 px-6 py-5 backdrop-blur md:-mx-12 md:px-12 lg:sr-only lg:relative lg:top-auto lg:mx-auto lg:w-full lg:px-0 lg:py-0 lg:opacity-0";
export const sectionHeaderClasses: string =
"text-md font-bold uppercase tracking-widest text-slate-200 lg:sr-only";
6. main-sections/ProjectsSection.tsx
This component receives the resolved value of projects
stored in state in my page.tsx
component. In order to get that list of projects, I retrieved data directly from my GitHub profile as I mentioned earlier. Since I wanted to structure that data in a specific way, I made sure the shape of the props for this component had only what I deemed necessary:
import {
contentDateClasses,
contentDivClasses,
contentLinkClasses,
divHeaderWrapperClasses,
sectionClasses,
sectionHeaderClasses,
tagClasses,
} from "./styles";
type GitHubData = {
id: string;
name: string;
language: string;
html_url: string;
pushed_at: string;
homepage: string;
description: string;
languages_url: string;
};
type ProjectsProps = {
projects: Array<GitHubData>;
};
export const ProjectsSection: React.FC<ProjectsProps> = (
props: ProjectsProps
) => {
const { projects } = props;
const formatDate = (date: Date) => date.toDateString();
return (
<section id="projects" className={sectionClasses} aria-label="Projects">
<div className={divHeaderWrapperClasses}>
<h2 className={sectionHeaderClasses}>Projects</h2>
</div>
<div>
<ol className="group/list">
{projects.map((project) => (
<a
key={project.name}
href={project.homepage ? project.homepage : project.html_url}
target="_blank"
>
<li key={project.id} className="mb-12">
<div className={contentDivClasses}>
<header
className={contentDateClasses}
aria-label={`${project.name} - ${formatDate(
new Date(project.pushed_at)
)}`}
>
Last updated on {formatDate(new Date(project.pushed_at))}
</header>
<div className="z-10 sm:col-span-6">
<h3 className="font-bold">
{project.name} {project.homepage && "🔗"}
</h3>
<div className={tagClasses}>{project?.description}</div>
</div>
</div>
</li>
</a>
))}
</ol>
<p className="text-center">
<a
href="https://github.com/cdwhitt"
target="_blank"
className={contentLinkClasses}
>
👉 View more on GitHub
</a>
</p>
</div>
</section>
);
};
Similar to my ExperienceSection
, this section uses a lot of the same re-usable classes I defined in my styles.ts
file. It looks like this on Desktop:
I don’t personally think this section turned out super fancy, but it’s effective enough for what I need to convey. If you have ideas related to how I can make this section flashier, please let me know!
7. main-sections/BlogSection.tsx
My blog section is pretty similar to my ProjectsSection
— There’s not really much worth noting here except for the fact that I’m also retrieving data from an external source to populate this component, so I needed to shape the props for this component:
import {
contentDateClasses,
contentDivClasses,
contentLinkClasses,
divHeaderWrapperClasses,
sectionClasses,
sectionHeaderClasses,
tagClasses,
} from "./styles";
type MediumData = {
author: string;
categories: Array<string>;
content: string;
description: string;
guid: string;
link: string;
pubDate: string;
thumbnail: string;
title: string;
};
type BlogProps = {
posts: Array<MediumData>;
};
export const BlogSection: React.FC<BlogProps> = (props: BlogProps) => {
const { posts } = props;
const formatDate = (date: Date) => date.toDateString();
return (
<section id="blog" className={sectionClasses} aria-label="Blog">
<div className={divHeaderWrapperClasses}>
<h2 className={sectionHeaderClasses}>Blog</h2>
</div>
<div>
<ol className="group/list">
{posts.map((post) => (
<a key={post.guid} href={post.link} target="_blank">
<li key={post.guid} className="mb-12">
<div className={contentDivClasses}>
<header
className={contentDateClasses}
aria-label={`${post.title} - ${formatDate(
new Date(post.pubDate)
)}`}
>
Published on {formatDate(new Date(post.pubDate))}
</header>
<div className="z-10 sm:col-span-6">
<h3 className="font-bold">{post.title}</h3>
<ul className="mt-2 flex flex-wrap" aria-label="Categories">
{post.categories.map((cat) => (
<li key={cat} className="mr-1.5 mt-2">
<div
className={tagClasses}
style={{
borderBottom: "5px solid #347BFF",
}}
>
{cat.toUpperCase()}
</div>
</li>
))}
</ul>
</div>
</div>
</li>
</a>
))}
</ol>
<p className="text-center">
<a
href="https://medium.com/@cwhitt91"
target="_blank"
className={contentLinkClasses}
>
👉 Read more on Medium
</a>
</p>
</div>
</section>
);
};
8. main-sections/FooterSection.tsx
Finally, the last component: the footer, where I link to my inspiration for this portfolio as well as to the technologies I used to build it. Here you can also email me directly by clicking on my homemade pixel art icons:
import { contentLinkClasses } from "./styles";
import Image from "next/image";
export const FooterSection: React.FC = () => {
return (
<footer className="max-w-md pb-16 text-sm text-black sm:pb-0">
<p>
This portfolio was inspired by{" "}
<a
href="https://brittanychiang.com/"
target="_blank"
className={contentLinkClasses}
>
Brittany Chiang
</a>
, who is way better than me at layout design! All content, projects,
writings, and styles used in this portfolio are my own. Built with{" "}
<a
href="https://nextjs.org"
target="_blank"
className={contentLinkClasses}
>
Next.js
</a>
,{" "}
<a
href="https://tailwindcss.com/"
target="_blank"
className={contentLinkClasses}
>
Tailwind CSS
</a>
, and deployed with{" "}
<a
href="https://vercel.com/"
target="_blank"
className={contentLinkClasses}
>
Vercel
</a>
.{" "}
<a href="cwhitt91@gmail.com" target="_blank">
<Image
src={"/heart.png"}
height={15}
width={15}
alt={""}
className="inline-block"
/>{" "}
<Image
src={"/email.gif"}
height={20}
width={20}
alt={"Get in touch!"}
className="inline-block"
/>
</a>
</p>
</footer>
);
};
Final Thoughts & Future Iterations
Building this portfolio was so much fun for me as I’ve never worked with Tailwind prior to now. I got a chance to learn a new framework that has a tremendous number of use-cases for building awesome UIs. Tailwind provides a large toolbox of pre-built classes that anyone with enough CSS knowledge can customize for their own use cases.
As a result, I ended up building a site I’m very proud of, and I know that I’ll continue to tweak it down the line. Perhaps I’ll create a dark mode for my next iteration.
If you made it this far and are interested in looking at the deployed version, or even building a similar type of site for yourself, please connect with me on LinkedIn and GitHub. You can find the repo for this project here. I plan to update it regularly now that it is out in the world.