Project Deep Dive

MassDev Portfolio

My portfolio showcases my web development projects with a simple, minimalistic design. Built with Next.js, Sass, and Sanity, it organizes projects into three categories:

  • Main: Primary and most significant projects.
  • Secondary: Notable but less critical projects.
  • Sandbox: Experiments with new technologies.

Each category is represented by a card detailing key features, technologies, and outcomes. The site also includes a blog where I analyze challenges, code solutions, and lessons learned, offering insight into my technical growth.

Challenges & solutions
01

Categorization of Projects: Structuring and Displaying by Importance

Challenge

When I thought about creating my personal portfolio, I believed it would be useful to categorize my projects both for myself, to structure them logically and easily update them, and for visitors to the site, to give them a clear and immediate representation of the projects based on their relevance. The categories I chose are "Main", "Secondary", and "Sandbox", which reflect the importance or development stage of each project.

Solution

To achieve this, I leveraged Sanity as my CMS to manage the project data and used the importance field to define which category each project belongs to. The importance field has three possible values: "main", "secondary", and "sandbox", representing the core, secondary, and experimental projects. In the ProjectList.tsx component, I wrote logic to fetch the projects from Sanity using a GROQ query, then grouped them by category (importance). Finally, I displayed each project group separately.

Detailed Explanation

Let’s take a detailed look at how I implemented this.

1. Defining the Project Model in Sanity

In Sanity, I created a project model with a field called importance, which allows me to categorize each project into one of the following categories: "Main", "Secondary", and "Sandbox". Here's how the project model is structured:

jsx
const project = {
  name: "project",
  title: "Projects",
  type: "document",
  fields: [
    {
      name: "importance",
      title: "Importance",
      type: "string",
      options: {
        list: [
          { title: "Main", value: "main" },
          { title: "Secondary", value: "secondary" },
          { title: "Testing New Technologies", value: "sandbox" },
        ],
        layout: "radio",
      },
    },
    // Other fields like "name", "technologies", "content", "status", etc.
  ],
};

With this structure, each project can easily be categorized, making the data management efficient.

2. GROQ Query to Fetch the Data

To fetch the projects from the Sanity CMS, I used a GROQ query. This query pulls the relevant data, including the project's name, technologies, status, content, and category (importance).

Here’s how the query is structured:

groq
export async function getProjects(): Promise<Project[]> {
  const projects = await client.fetch(
    groq`*[_type == "project"]{
      name,
      _id,
      importance,
      "technologies": technologies[]-> { _id, name, icon },
      "challenges": challenges[]-> { title, description }
    }`
  );
  return projects;
}

The query returns all projects, including the technologies and challenges. Using the importance field, we can easily group the projects by category.

3. Grouping Projects by Category in ProjectList.tsx

In the ProjectList.tsx component, I implemented logic to group the projects by their importance value using useMemo. This ensures that projects are displayed separately for each category.

Here’s how the grouping is implemented:

javascript
const groupedProjects = useMemo(() => {
  return projects.reduce((acc, project) => {
    if (!acc[project.importance]) acc[project.importance] = [];
    acc[project.importance].push(project);
    return acc;
  }, {} as Record<string, Project[]>);
}, [projects]);

This way, the projects are grouped into separate objects based on their category. Each category (main, secondary, sandbox) will have its own group of projects.

4. Displaying Projects

Once grouped, the projects are displayed dynamically in the JSX, separated by category. Here’s how the rendering happens:

javascript
<div className="projectListContainer">
  {Object.keys(groupedProjects).map((category) => (
    <div key={category} className="categorySection">
      <h2>{category}</h2>
      <div className="projectCards">
        {groupedProjects[category].map((project) => (
          <ProjectCard key={project._id} project={project} />
        ))}
      </div>
    </div>
  ))}
</div>

Each group of projects is rendered in its own section, with the category name as a heading.

Conclusion and Potential Improvements

This solution allows for a simple and scalable management of projects, with the ability to easily add new projects and assign them to one of the three categories. Using Sanity as the CMS makes the structure highly flexible, enabling data modification without touching the code.

One potential improvement could be adding a filtering system to display projects based on their technologies or other properties. Additionally, we could introduce a sorting system to show the most recent or most important projects first.

02

Implementing Project Filtering by Used Technology

Challenge

In my portfolio, I wanted to implement a filtering system for projects based on the technologies used. The goal was to allow users to select one or more technologies and display only the projects built with them.

Solution

To achieve this goal, I created a component called ProjectFilter.tsx, which allows users to select available technologies and dynamically filter the project list. The component uses useState to manage the selected technologies, useMemo to optimize filtering, and useEffect to update the state of the filtered projects.

Introduction to the Project Filter Component

This component, ProjectFilter, is designed to allow users to filter a list of projects based on selected technologies. It dynamically updates the displayed projects based on the user’s filter choices and ensures that the UI remains responsive and efficient. It is part of a larger application built with React, Next.js, and Sanity.

Here’s a breakdown of how it works:

  1. Managing the User's Selection: The component allows users to select one or more technologies from a list. Based on the selected technologies, the list of displayed projects is filtered to only show the ones that match all of the selected criteria.
  2. Optimizing Performance: To ensure the filtering process is efficient, useMemo is used to memoize the list of unique technologies and the filtered list of projects, reducing unnecessary recalculations.
  3. Interactive UI: The user interface consists of a set of buttons corresponding to each technology. The buttons are toggled on and off based on the user’s selection, and there’s a clear button to reset the selection.

With that overview, let’s look at the code in detail, starting with how we manage the state in the component.

1. State Management with useState

javascript
const [selectedTechnologies, setSelectedTechnologies] = useState<string[]>([]);

This line declares a state variable selectedTechnologies and its setter function setSelectedTechnologies. It is initialized as an empty array of strings. This state will hold the list of technologies that the user selects to filter the projects. The state is an array because multiple technologies can be selected at once.

useState is used here to allow React to track and re-render the component whenever the list of selected technologies changes.

2. Creating a List of Unique Technologies with useMemo

javascript
const uniqueTechnologies = useMemo(
  () => Array.from(new Set(projects.flatMap((p) => p.technologies?.map((t) => t.name) || []))),
  [projects]
);

This block of code creates an optimized list of unique technologies from all the projects available.

  • projects.flatMap(...): Flattens the technologies array from each project (p) and extracts the technology names (t.name). The flatMap method ensures that all technology names are in a single array.
  • new Set(...): A Set automatically removes any duplicate technology names. By wrapping the result of flatMap with a Set, we ensure that only unique technology names are kept.
  • Array.from(...): Converts the Set back into an array.

useMemo is used to memoize the list of unique technologies. This ensures that the list is recalculated only when the projects array changes, preventing unnecessary recalculations on each render, which improves performance.

3. Filtering the Projects

javascript
const filteredProjects = useMemo(() => {
  if (!selectedTechnologies.length) return projects;
  return projects.filter((p) =>
    selectedTechnologies.every((tech) => p.technologies?.some((t) => t.name === tech))
  );
}, [selectedTechnologies, projects]);

This part is responsible for filtering the projects based on the selected technologies.

  • If no technologies are selected (selectedTechnologies.length is 0), the function returns the original list of projects (projects) without any filtering.
  • If technologies are selected, the projects.filter(...) method is used to filter the projects. It iterates through each project and checks if it contains all of the selected technologies:
    • selectedTechnologies.every(...): The every method ensures that the project contains every selected technology.
    • p.technologies?.some((t) => t.name === tech): For each selected technology (tech), the project (p) is checked to see if it contains a technology with the same name.

useMemo ensures that the filtered list is recomputed only when the selectedTechnologies or projects change, avoiding unnecessary re-renders.

4. Updating the Filtered Projects on Selection Change

javascript
useEffect(() => {
  setFilteredProjects(filteredProjects);
  setOpenProjectId(null);
}, [filteredProjects, setFilteredProjects, setOpenProjectId]);

This useEffect hook is used to update the parent component whenever the list of filtered projects changes.

  • setFilteredProjects(filteredProjects): It updates the parent component’s state with the new filtered projects, ensuring that the parent has the latest list of filtered results.
  • setOpenProjectId(null): Resets the currently open project (if any) by setting its ID to null. This ensures that when the filter changes, the currently selected project (if any) is reset.

The hook runs whenever the filteredProjects changes, which triggers a re-render and updates the parent component.

5. Handling No Matching Projects

javascript
const noProjectsMatch = selectedTechnologies.length > 0 && filteredProjects.length === 0;

This line checks if there are any projects that match the selected technologies.

  • If there are selected technologies (selectedTechnologies.length > 0), and the filtered list of projects is empty (filteredProjects.length === 0), it sets noProjectsMatch to true.
  • This variable will be used to conditionally display a message that no projects match the selected technologies.

6. Toggling Technology Selection

javascript
const toggleTechnology = (tech: string) => {
  setSelectedTechnologies((prev) =>
    prev.includes(tech) ? prev.filter((t) => t !== tech) : [...prev, tech]
  );
};

This function is responsible for toggling the selection of a technology.

  • If the technology (tech) is already in the selectedTechnologies array, it is removed using prev.filter((t) => t !== tech).
  • If the technology is not in the list, it is added using [...prev, tech].

This ensures that the user can select or deselect technologies to filter the projects dynamically.

7. Clearing the Selected Technologies

javascript
const clearSelection = () => setSelectedTechnologies([]);

This function clears all selected technologies by setting the selectedTechnologies state to an empty array. This is triggered when the user clicks the "Clear" button to reset the filter.

8. Rendering the Filter Buttons and No Projects Message

javascript
return (
  <div className={styles.filterContainer}>
    <section className={styles.filter} aria-label="Filter projects by technology">
      {uniqueTechnologies.map((tech) => (
        <button
          key={tech}
          onClick={() => toggleTechnology(tech)}
          className={`${styles.theButton} ${selectedTechnologies.includes(tech) ? styles.active : ""}`}
          aria-pressed={selectedTechnologies.includes(tech) ? "true" : "false"}
        >
          {tech}
        </button>
      ))}
      {selectedTechnologies.length > 0 && (
        <span className={styles.clearButtonContainer}>
          <button className={styles.clearButton} onClick={clearSelection} aria-label="Clear selection">
            Clear
          </button>
        </span>
      )}
    </section>
    {noProjectsMatch && (
      <NoProjectsMessage
        selectedTechnologies={selectedTechnologies}
        handleClearSelection={clearSelection}
      />
    )}
  </div>
);

This block renders the user interface of the filter component.

  • The filter buttons are generated dynamically from the uniqueTechnologies array. Each button has an onClick event that calls toggleTechnology to add or remove the technology from the selected list.
  • The buttons are styled differently if they are selected, using the class active.
  • If there are selected technologies, a "Clear" button is displayed, which allows the user to reset the filter.
  • If no projects match the selected technologies, a message is displayed using the NoProjectsMessage component.

Conclusion

This code efficiently implements a filtering system for projects based on selected technologies. By using useMemo and useEffect, it ensures the filter operates efficiently even with larger datasets, and the UI remains responsive. The ability to dynamically select or deselect technologies, as well as clear the filter, provides a smooth and intuitive user experience.

03

Interactive Project Detail Card with Dynamic Navigation

Challenge

The goal was to make the project card in my portfolio more interactive and structured by dividing it into two sections. The first section needed to contain a general description of the project along with the technologies used, while the second section should be accessible through a click. This section would list the challenges faced during the development of the project. The links within this section were designed to direct the user to the project blog page, then scroll to the specific point related to the challenge using a double timeout animation. Additionally, the same links would be reused inside the blog page, enabling easy navigation within the page.

Solution

I split the project card into two distinct sections, implementing the descriptive part in the first section and the challenge list in the second. I used conditional rendering to display the list of challenges only when the user clicks on the respective section. The links were created to first lead to the project blog page, and then, through the use of a double timeout, scroll the page to the location of the selected topic. Inside the blog, the same links are reused, allowing for easy navigation between different sections of the page.

1. Managing the Element’s State

To manage the opening and closing of the project card, I use the useState hook to track the ID of the open project. When a project is clicked, the project’s ID is stored in the state, and the card opens. If the same project is clicked again, the card closes.

javascript
const [isExiting, setIsExiting] = React.useState(false);

const toggleProjectInfoHandler = useCallback((projectId: string) => {
  if (openProjectId === projectId) {
    setIsExiting(true);
    setTimeout(() => {
      toggleProjectInfo(null);
      setIsExiting(false);
    }, 500);
  } else {
    toggleProjectInfo(projectId);
  }
}, [openProjectId, toggleProjectInfo]);
  • isExiting: A state that manages whether the card is exiting (in animation).
  • toggleProjectInfoHandler: A function that handles opening and closing the card, with a delay for the exit animation.

2. Opening and Closing Animation

The opening and closing animations are handled via CSS animations. I use the fadeInUp and fadeOutDown animations to apply a smooth transition effect when the card appears and disappears.

sass
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes fadeOutDown {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(20px);
  }
}

The animations are applied to the .infoBubble class for the opening, and to the .infoBubble.exit class for the closing.

javascript
{openProjectId === project._id && (
  <div className={`${styles.infoBubble} ${isExiting ? styles.exit : ""}`}>
    <ProjectInfo project={project} openProjectId={openProjectId} />
  </div>
)}

In the CSS, the .infoBubble class is responsible for the opening animation, while the .exit class is dynamically added when the card is in the process of closing (exiting).

CSS Code:

sass
.infoBubble {
  background-color: #fafbff;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1000;
  border: 2px solid $primary-color;
  opacity: 0;
  animation: fadeInUp 0.5s ease forwards;
  transition: opacity 0.5s ease;
}

.infoBubble.exit {
  animation: fadeOutDown 0.5s ease forwards;
}

.infoBubble: This class applies the fadeInUp animation, which makes the card appear with a sliding-up effect..infoBubble.exit: When the card is closing, the exit class is added, applying the fadeOutDown animation to make the card disappear with a sliding-down effect.

So, when the user clicks on the card, additional information appears, which we will analyze below with the ProjectInfos component.

1. ProjectInfo.tsx Component

The ProjectInfo.tsx component contains the project details, including information about the challenges encountered. When the user clicks on a challenge, a function is triggered that navigates to the project page and, after a short delay, scrolls to the specific challenge section.

2. How the Challenge Links Work

The links for the challenges are generated inside the component as a list of buttons. Each button represents a challenge, and when clicked, the handleScrollToChallenge function is called to handle the navigation and scrolling.

Code Details:

  1. Navigate to the project: The handleScrollToChallenge function handles navigating to the project page (/projects/${project.slug}) using router.push.
  2. Scroll to the specific section: After navigating to the project page, a delay is executed (using setTimeout) to allow the page to fully load, and then a second setTimeout scrolls to the challenge section, identified by a unique ID for each challenge.
javascript
const handleScrollToChallenge = (challengeId: string) => {
  // Navigate to the project page
  router.push(`/projects/${project.slug}`);
  
  // After a short delay, scroll to the challenge section
  setTimeout(() => {
    setTimeout(() => {
      // Find the challenge element by its generated ID
      const element = document.getElementById(challengeId);
      if (element) {
        element.scrollIntoView({
          behavior: "smooth", // Smooth scroll effect
          block: "start",     // Align the element at the top of the page
        });
      }
    }, 500); // Delay to ensure the page is fully loaded
  }, 300); // Initial delay after navigation
};

3. Generating Challenge IDs

Each challenge has a unique ID that is created by the generateChallengeId function. The ID is formed from the title of the challenge, where spaces are replaced with dashes, and the text is converted to lowercase.

javascript
const generateChallengeId = (title: string) => `challenge-${title.replace(/\s+/g, "-").toLowerCase()}`;

4. Rendering Challenge Buttons

In the ProjectInfo component, if the project has challenges, buttons are rendered for each challenge. Each button, when clicked, calls the handleScrollToChallenge function, passing the challenge ID as a parameter.

javascript
{project.challenges.map((challenge) => (
  <li key={challenge._id} className={styles.challengesListItem}>
    <button className={styles.buttonChallange} onClick={() => handleScrollToChallenge(generateChallengeId(challenge.title))}>
      {challenge.title}
    </button>
  </li>
))}

5. Navigation and Scrolling

The complete flow of behavior is as follows:

  1. The user clicks on a button related to a challenge.
  2. The handleScrollToChallenge function is executed:
    • It first navigates to the project page (/projects/${project.slug}).
    • After a short delay, the function scrolls to the specific challenge section.
  3. The page will automatically scroll to the challenge element using scrollIntoView.

The ProjectInfo.tsx component handles displaying the project details and challenges. Each challenge is a link that, when clicked, first navigates to the project page and then scrolls to the corresponding challenge section. The two timeouts ensure the navigation and scrolling are synchronized, providing a smooth and error-free transition.