If you came across my portfolio site and are interested in the logic, idea, and overall working principle, then this documentation is for you. In this doc, I’ll walk you through the development of my portfolio site, including all the languages used, the progression of the project’s lifecycle, the logic behind certain implementations, performance strategies and how I arrived at a satisfactory output.
So, sit back, grab a coffee, and let’s go on a ride because this won’t be the cliché technical documentation. I’ll also be revealing some personal—not so technical—decisions I made in the development of this project.
Getting Started
The first information you’d want to know about this project is the technology stack used in development. I’d also mention that this project was initially built using React, JavaScript and CSS modules but I only recently converted them to a new technology stack. The new stack includes:
React
TypeScript
Framer Motion
Tailwind CSS
React-type animation.
One of the major considerations in the development of this project was simplicity; from the design to the code, you’ll see more about that in a bit. If you want to run this project locally, you can fork the repository; you can also view the project live. Note that you’d have to install React and all the necessary dependencies to run the project locally.
Loading Page
Here’s the first screen you see when you click on the live link, however, it lasts only 5 seconds. I added this just for aesthetics and to improve the user experience. Although there are a couple of libraries that offer this feature, I chose to manually utilize pre-built Tailwind CSS animation classes and conditionally render it. Here’s what the Loading.tsx
component looks like:
import React from "react";
const Loading: React.FC = () => {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<div className="animate-bounce text-3xl md:text-5xl border-gray-900 text-white font-bold">
<span>james</span> {""}
<span className="text-[#09c]">amoo</span>
</div>
</div>
);
};
export default Loading;
Super easy, isn’t it? I believe this method is better than the use of external libraries which may further affect the performance of your application. The code above simply creates a bouncing up-and-down motion with my name. Now all we have to do is conditionally render this page in our App.tsx
. Here’s the code:
// imports here
function App() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setTimeout(() => {
setIsLoading(false);
}, 5000);
}, []);
return (
<Router>
<div className="App">
{isLoading ? (
<Loading />
) : (
<>
<Navbar />
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route exact path="/projects">
<Projects />
</Route>
<Route exact path="/articles">
<Articles />
</Route>
</Switch>
</>
)}
</div>
</Router>
);
}
export default App;
In the code above, useState
controls the state of the rendering. isLoading
is set to true
by default so the Loading
component mounts in the DOM. The duration is controlled by useEffect
and the setTimeout
JavaScript function. The value is set to 5000ms(5 seconds) which means the isLoading
state is true for only 5 seconds and then it is set to false. When isLoading
is true, the Loading component displays, when it is set to false, the rest of the content is rendered.
Home Page
As I mentioned earlier, I was focused on making this project as simple as possible. From the design, you’ll see I have just three pages; the home page(the default page), the projects page and the articles page. I also included social links where you can reach me. Let’s take a deeper dive into the components of the home page.
Avatar
I had initially put my picture in place of this(for over two years) but I decided to try something new. My avatar was made using Cartoonize, a free online avatar-maker. The process of customizing and creating the avatar was fairly easy for me. I spent quite some time trying to get the perfect representation of my face but was faced with the major shortcoming of not seeing the option to include beards. To some extent, the avatar adequately represents my face(excluding my beard, haha).
TypeWriter Feature
When you open the live link of this project, you see that the title(which is “Software Engineer” in the image above) infinitely switches across three titles(values). This was implemented using the react-type-animation library. Here’s how the code looks:
import { TypeAnimation } from "react-type-animation";
const typeAnimationSequence = [
"Software Engineer",
1000,
"Machine Learning Engineer",
1000,
"First Class Computer Science Graduate",
1000,
];
// jsx
<TypeAnimation
sequence={typeAnimationSequence}
wrapper="span"
speed={50}
repeat={Infinity}
/>
Here, the TypeAnimation
is imported from the library. For simplicity and to improve code readability, I separately define the sequence value which takes in two arguments; the title content and the duration. This makes it easier to use in jsx
, where I simply define the speed and set the repeat to infinity, so it repeatedly runs.
Socials
The last component to dive into is the socials button, which is located at the bottom of the home page. It includes icons from react-icons
which represent links to my GitHub page, X account, email, and LinkedIn profile. The code in this component is fairly straightforward. First I save the data using TypeScript interfaces:
interface SocialLink {
href: string;
label: string;
icon: React.ReactNode;
}
const iconstyle = ["text-xl", "md:text-3xl"];
const socialLinks: SocialLink[] = [
{
href: "https://github.com/ayothekingg",
label: "Link to Github",
icon: <AiFillGithub className={iconstyle.join(" ")} />,
},
{
href: "https://twitter.com/",
label: "Link to Twitter",
icon: <FaTwitter className={iconstyle.join(" ")} />,
},
{
href: "mailto:ayojames444@gmail.com",
label: "Link to Email",
icon: <MdOutlineAlternateEmail className={iconstyle.join(" ")} />,
},
{
href: "https://www.linkedin.com/in/jamesamooo/",
label: "Link to LinkedIn",
icon: <BsLinkedin className={iconstyle.join(" ")} />,
},
];
I thought about putting this in a separate doc like I did in Projects.tsx
(you’ll see that in a bit) but I was keen on maintaining the folder structure of the components folder. Now that this is defined, I easily map through the data above:
const Socials: React.FC = () => {
return (
<motion.div initial="hidden" animate="visible" variants={socialsMotion}>
<ul className="m-0 p-0 pt-3 flex justify-center float-none mt-30 md:mt-0">
{socialLinks.map(({ href, label, icon }) => (
<li key={href}>
<a href={href} target="_blank" rel="noreferrer" aria-label={label}>
{icon}
</a>
</li>
))}
</ul>
</motion.div>
);
};
Rather than explicitly defining the data for each link in the body, it’s easier to define the data first and then map through it. This particularly improves the maintainability of the component as you can just update the data without altering the body of the code.
Projects Page
The projects page contains some of the projects I’ve worked on in the past. I take a different approach here by creating a separate file which contains the data, and then importing them in the Projects.tsx
file. First, I define the types in the types.ts
file:
export interface ProjectData {
title: string;
description: string;
image: string;
technologies: string[];
links: {
live: string;
code: string;
};
}
The types.ts
file contains the types of data to be expected. Following this declaration, I can create the actual data in my index.ts
file like so:
import { ProjectData } from "./types";
export const projectsData: ProjectData[] = [
{
title: "SHOPTACLE",
description: `Designed by Zaynab Ogunnubi, Shoptacle is a full stack application that displays the basic functionalities of an ecommerce site. `,
image: "/shoptacle.JPG",
technologies: ["React", "Tailwind CSS", "Firebase", "CommerceJS", "Stripe"],
links: {
live: "https://shoptaclestore.netlify.app",
code: "https://github.com/ayothekingg/shoptacle",
},
// other data
The above code is the template for the data created in the index.ts
file, the final action is to import the data and map through them in the Projects.tsx
file. Here:
import { motion } from "framer-motion";
import { projectsData } from ".";
export default function Projectss() {
return (
<div className="flex flex-col mx-10 md:mx-20 items-center md:flex-col">
{projectsData.map((project, index) => (
<div className="flex justify-start flex-[0 0 40%]">
<div>
<img
src={project.image}
alt={project.title}
className="w-96 h-44 rounded-md"
/>
</div>
</div>
<div className="flex-1 w-full">
<h3 className="font-medium text-lg tracking-wide m-0">
{project.title}
</h3>
<h3 className="border border-gray-400 mb-2">
{project.description}
</h3>
<div className="text-white mb-5 rounded-sm *:mr-2 *:tracking-widest *:rounded-md *:text-xs *:shadow-md">
{project.technologies.map((tech) => (
<h3 className="inline" key={tech}>
{tech}
</h3>
))}
</div>
<div className="*:mr-5">
<span className="mt-0 cursor-pointer p-0 bg-[#4b4343] text-center">
<a href={project.links.live} target="_blank" rel="noreferrer">
<button className="w-[100px] h-auto bg-transparent text-white font-poppins outline-none mt-auto cursor-pointer text-sm border-none border border-[#707070] rounded-md">
Live
</button>
</a>
</span>
<span className="mt-0 cursor-pointer p-0 bg-[#4b4343] text-center">
<a href={project.links.code} target="_blank" rel="noreferrer">
<button className="w-[100px] h-auto bg-transparent text-white font-poppins outline-none mt-auto cursor-pointer text-sm border-none border border-[#707070] rounded-md">
Code
</button>
</a>
</span>
</div>
</div>
</motion.div>
))}
</div>
);
}
The above code represents the template of the projects page where I easily map through the data that has been imported.
As you scroll downward on the Projects page, you see that the projects cards get revealed with continuous scroll action, this was done using framer motion, particularly using the whileInView
function. Here’s the code for the implementation:
<motion.div
className="flex flex-col md:flex-row text-center items-center justify-center md:text-start max-w-[1000px] gap-5 md:gap-20 mt-12 w-full rounded-lg shadow-[4px_4px_0_0_rgb(46,44,44)]"
variants={cardVariants}
initial="hidden"
whileInView="visible"
viewport={{ amount: 0.2 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
The initial state of the projects is hidden but becomes visible when it’s in view; that is the animation triggers when some portion of the content is in view. This can be defined by using the viewport
value which is set to 0.2
in the code. That means the animation triggers when 20% of the element is in view. You can set a custom value to this which fits your preference. Finally, we set the values for the duration and delay of the animation.
Articles
The articles page contains a list of all my published articles on Hashnode. I implement this by using the Hashnode GraphQL API which allows you directly make use of Hashnode’s resources right from your local code. The connection process to Hashnode’s API was somewhat strenuous, especially since they had major changes to their legacy API which broke the connection I had used initially. First, I define all the types to be used:
interface Post {
node: {
title: string;
slug: string;
brief: string;
coverImage: {
url: string;
};
};
}
Then I make a connection to the API by making a POST
request, fetching all the data to be used:
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch("https://gql.hashnode.com", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query GetUserArticles {
user(username: "jamesamoo") {
publications(first: 10, filter: { roles: OWNER }) {
edges {
node {
posts(first: 10) {
edges {
node {
title
slug
brief
coverImage {
url
}
}
}
}
}
}
}
}
}
`,
}),
});
const data = await response.json();
setPosts(data.data.user.publications.edges[0].node.posts.edges);
} catch (error) {
console.error("Error fetching posts:", error);
}
};
fetchPosts();
}, []);
To get your Hashnode public data, you can pass your username as an argument to the user
query. To get some of your personal data, you will require a private key which can be gotten from the developers section of your dashboard.
I prefer the legacy Hashnode API to the new one, especially since I didn’t have to go so deep in nodes and edges just to get user details to display. The code above basically fetches the title, slug, brief, and cover image of the username which you provide(Note that there are a variety of data that the Hashnode API provides. I specifically require these. After a request and connection to the API, you can then represent the data in the body of your file. Here’s what the code looks like in the body of the Articles.tsx
file:
<div className="flex flex-col items-center mx-5 my-10">
<h3>
All articles are fetched from my{" "}
<a
className="text-blue-500"
href="https://jamesamoo.hashnode.dev/"
target="_blank"
rel="noreferrer"
>
Hashnode account
</a>
</h3>
{posts.map((post) => (
<motion.div
variants={cardVariants}
initial="hidden"
whileInView="visible"
viewport={{ amount: 0.2 }}
transition={{ duration: 0.5, delay: 0.1 }}
key={post.node.slug}
className="flex flex-col items-center w-full max-w-4xl mt-10 md:flex-row rounded-md shadow-md gap-8 md:gap-0 text-center md:text-start"
>
<div className="flex justify-start md:w-2/5">
<img
src={post.node.coverImage.url}
alt={post.node.title}
className="w-80 h-45 rounded"
/>
</div>
<div className="w-full md:w-3/5">
<h3 className="m-0 text-lg font-medium tracking-widest uppercase">
{post.node.title}
</h3>
<p className="mb-1 border border-gray-500 text-white">
{post.node.brief}{" "}
<a
className="text-blue-500"
href={`https://jamesamoo.hashnode.dev/${post.node.slug}`}
target="_blank"
rel="noreferrer"
>
Read More
</a>
</p>
</div>
</motion.div>
))}
</div>
);
};
Conclusion
I’ll conclude this by saying if there’s a correction you’d want me to make, please feel free to create an issue on GitHub. You can also create a pull request if you’d like to make some enhancements or optimization to the code. This doc will be posted on my Hashnode account and will also be added to the read.me file of this project. Thanks!