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.
Categorization of Projects: Structuring and Displaying by Importance
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.
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:
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:
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:
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:
<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.
Implementing Project Filtering by Used Technology
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.
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:
- 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.
- Optimizing Performance: To ensure the filtering process is efficient,
useMemois used to memoize the list of unique technologies and the filtered list of projects, reducing unnecessary recalculations. - 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
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
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 thetechnologiesarray from each project (p) and extracts the technology names (t.name). TheflatMapmethod ensures that all technology names are in a single array.new Set(...): ASetautomatically removes any duplicate technology names. By wrapping the result offlatMapwith aSet, we ensure that only unique technology names are kept.Array.from(...): Converts theSetback 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
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.lengthis 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(...): Theeverymethod 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
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 tonull. 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
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 setsnoProjectsMatchtotrue. - This variable will be used to conditionally display a message that no projects match the selected technologies.
6. Toggling Technology Selection
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 theselectedTechnologiesarray, it is removed usingprev.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
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
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
uniqueTechnologiesarray. Each button has anonClickevent that callstoggleTechnologyto 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
NoProjectsMessagecomponent.
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.