v0.12.4 Update Project card to filter with array categories, and polish project card presentation

This commit is contained in:
Murtadha 2024-07-28 14:22:02 -04:00
parent 7875d92b4c
commit 9ccb1d5a1d
5 changed files with 201 additions and 37 deletions

View file

@ -17,13 +17,16 @@ function Projects({ title, data }) {
}, []);
const categories = useMemo(() => {
const cats = new Set(data.map((project) => project.category));
const cats = new Set(data.flatMap((project) => project.category));
return ["All", ...Array.from(cats)];
}, [data]);
const filteredProjects = useMemo(() => {
if (activeFilter === "All") return data;
return data.filter((project) => project.category === activeFilter);
const sortedAndFilteredProjects = useMemo(() => {
let filteredProjects = activeFilter === "All"
? data
: data.filter((project) => project.category.includes(activeFilter));
return filteredProjects.sort((a, b) => b.date - a.date);
}, [data, activeFilter]);
const handleFilterClick = (category) => {
@ -41,14 +44,23 @@ function Projects({ title, data }) {
<h2 className={styles.sectionTitle}>{title}</h2>
<div className={styles.filterContainer}>
{categories.map((category) => (
<button key={category} className={`${styles.filterButton} ${activeFilter === category ? styles.active : ""}`} onClick={() => handleFilterClick(category)}>
<button
key={category}
className={`${styles.filterButton} ${activeFilter === category ? styles.active : ""}`}
onClick={() => handleFilterClick(category)}
>
{category}
</button>
))}
</div>
<div className={styles.projectGrid}>
{filteredProjects.map((project) => (
<ProjectCard key={project.id} project={project} onClick={openModal} className={animatingOut ? styles.fadeOut : styles.fadeIn} />
{sortedAndFilteredProjects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onClick={openModal}
className={animatingOut ? styles.fadeOut : styles.fadeIn}
/>
))}
</div>
{selectedProject && <ProjectModal project={selectedProject} onClose={closeModal} />}
@ -56,4 +68,4 @@ function Projects({ title, data }) {
);
}
export default Projects;
export default Projects;

View file

@ -1,7 +1,44 @@
import React from "react";
import React, { useRef, useEffect, useState } from "react";
import styles from "./ProjectCard.module.css";
function ProjectCard({ project, onClick, className }) {
const [truncatedDescription, setTruncatedDescription] = useState(project.description);
const descriptionRef = useRef(null);
const formatList = (items) => {
return items.map((item, index, arr) => (
<React.Fragment key={index}>
{item}
{index < arr.length - 1 && <span className={styles.separator}>, </span>}
</React.Fragment>
));
};
useEffect(() => {
const truncateDescription = () => {
const element = descriptionRef.current;
if (!element) return;
const maxHeight = parseInt(window.getComputedStyle(element).lineHeight) * 4; // 4 lines
let text = project.description;
element.textContent = text;
while (element.scrollHeight > maxHeight && text.length > 0) {
text = text.slice(0, -1);
element.textContent = text + '...';
}
setTruncatedDescription(element.textContent);
};
truncateDescription();
window.addEventListener('resize', truncateDescription);
return () => {
window.removeEventListener('resize', truncateDescription);
};
}, [project.description]);
return (
<div className={`${styles.card} ${className}`} onClick={() => onClick(project)}>
<div className={styles.imageSlider}>
@ -9,11 +46,17 @@ function ProjectCard({ project, onClick, className }) {
</div>
<div className={styles.content}>
<h3 className={styles.title}>{project.title}</h3>
<p className={styles.category}>{project.category}</p>
<p className={styles.description}>{project.description}</p>
<div className={styles.categories}>
{formatList(project.category)}
</div>
<p ref={descriptionRef} className={styles.description}>{truncatedDescription}</p>
<div className={styles.technologies}>
{formatList(project.technologies)}
</div>
{/* <p className={styles.date}>{project.date}</p> */}
</div>
</div>
);
}
export default ProjectCard;
export default ProjectCard;

View file

@ -5,9 +5,10 @@
overflow: hidden;
transition: transform 0.3s ease;
cursor: pointer;
height: 450px; /* Fixed height for the card */
display: flex;
flex-direction: column;
height: auto;
min-height: 450px;
}
.card:hover {
@ -32,7 +33,6 @@
display: flex;
flex-direction: column;
padding: 1rem;
overflow: hidden;
}
.title {
@ -42,7 +42,7 @@
color: #333;
}
.category {
.categories {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.5rem;
@ -51,10 +51,24 @@
.description {
font-size: 0.9rem;
color: #666;
flex-grow: 1;
margin-bottom: 0.5rem;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4; /* Adjust this number to show more or fewer lines */
-webkit-box-orient: vertical;
text-overflow: ellipsis;
line-height: 1.4;
max-height: calc(1.4em * 4); /* 4 lines of text */
}
.technologies {
font-size: 0.8rem;
color: #0066cc;
margin-bottom: 0.5rem;
}
.date {
font-size: 0.8rem;
color: #999;
margin-top: auto;
}
.separator {
margin: 0 2px;
}