Skip to content

Commit e4946bd

Browse files
committed
feat: add "more..." button to show additional projects
- Show only first 3 projects initially when more than 3 exist - Add link-style button positioned in bottom right corner - Reveal all remaining projects with staggered animations when clicked - Hide button after expansion - Maintain responsive grid layouts and existing animations
1 parent d37ba96 commit e4946bd

2 files changed

Lines changed: 185 additions & 20 deletions

File tree

src/components/sections/FeaturedProjects.tsx

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"use client";
22

33
import { motion } from 'framer-motion';
4-
import { ProjectCard } from '@/components/micro';
5-
import { projectStaggerVariants } from '@/lib';
4+
import { useState } from 'react';
5+
import { ProjectCard, Button } from '@/components/micro';
6+
import { projectStaggerVariants, projectCardVariants } from '@/lib';
67
import { type Project } from '@/content';
78

89
export interface FeaturedProjectsProps {
@@ -20,8 +21,15 @@ function getGridClasses(projectCount: number): string {
2021
}
2122

2223
export function FeaturedProjects({ projects }: FeaturedProjectsProps) {
24+
const [showAllProjects, setShowAllProjects] = useState(false);
2325
const projectCount = projects?.length || 0;
2426
const gridClasses = getGridClasses(projectCount);
27+
28+
// Show only first 3 projects initially, or all if less than 4
29+
const initialProjects = projects?.slice(0, 3) || [];
30+
const remainingProjects = projects?.slice(3) || [];
31+
const shouldShowMoreButton = projectCount > 3;
32+
const displayedProjects = showAllProjects ? projects : initialProjects;
2533

2634
return (
2735
<section
@@ -37,23 +45,60 @@ export function FeaturedProjects({ projects }: FeaturedProjectsProps) {
3745
Featured Projects
3846
</h2>
3947

40-
<motion.div
41-
className={`${gridClasses} gap-8 mt-12`}
42-
data-testid="projects-grid"
43-
variants={projectStaggerVariants}
44-
initial="hidden"
45-
whileInView="visible"
46-
viewport={{ once: true, amount: 0.3 }}
47-
>
48-
{projects?.map((project, index) => (
49-
<ProjectCard
50-
key={index}
51-
title={project.title}
52-
description={project.description}
53-
technologies={project.technologies}
54-
/>
55-
))}
56-
</motion.div>
48+
{!showAllProjects ? (
49+
<motion.div
50+
className={`${gridClasses} gap-8 mt-12`}
51+
data-testid="projects-grid"
52+
variants={projectStaggerVariants}
53+
initial="hidden"
54+
whileInView="visible"
55+
viewport={{ once: true, amount: 0.3 }}
56+
>
57+
{initialProjects?.map((project, index) => (
58+
<ProjectCard
59+
key={index}
60+
title={project.title}
61+
description={project.description}
62+
technologies={project.technologies}
63+
/>
64+
))}
65+
</motion.div>
66+
) : (
67+
<motion.div
68+
className={`${gridClasses} gap-8 mt-12`}
69+
data-testid="projects-grid"
70+
variants={projectStaggerVariants}
71+
initial="hidden"
72+
animate="visible"
73+
>
74+
{projects?.map((project, index) => (
75+
<ProjectCard
76+
key={index}
77+
title={project.title}
78+
description={project.description}
79+
technologies={project.technologies}
80+
/>
81+
))}
82+
</motion.div>
83+
)}
84+
85+
{shouldShowMoreButton && !showAllProjects && (
86+
<motion.div
87+
className="flex justify-end mt-8"
88+
data-testid="more-button-container"
89+
initial={{ opacity: 0, y: 20 }}
90+
animate={{ opacity: 1, y: 0 }}
91+
transition={{ duration: 0.5, delay: 0.5 }}
92+
>
93+
<Button
94+
variant="link"
95+
onClick={() => setShowAllProjects(true)}
96+
data-testid="more-projects-button"
97+
>
98+
more...
99+
</Button>
100+
</motion.div>
101+
)}
57102
</div>
58103
</section>
59104
);

src/components/sections/__tests__/FeaturedProjects.test.tsx

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from "@testing-library/react";
1+
import { render, screen, fireEvent } from "@testing-library/react";
22
import { FeaturedProjects } from "../FeaturedProjects";
33

44
jest.mock("@/components/micro");
@@ -60,6 +60,126 @@ describe("FeaturedProjects", () => {
6060
expect(projectCards).toHaveLength(0);
6161
});
6262

63+
describe("More Projects Functionality", () => {
64+
const manyProjects = [
65+
{
66+
title: "Test Project 1",
67+
description: "Test description 1",
68+
technologies: ["React", "Node.js"]
69+
},
70+
{
71+
title: "Test Project 2",
72+
description: "Test description 2",
73+
technologies: ["Next.js", "PostgreSQL"]
74+
},
75+
{
76+
title: "Test Project 3",
77+
description: "Test description 3",
78+
technologies: ["Vue", "MongoDB"]
79+
},
80+
{
81+
title: "Test Project 4",
82+
description: "Test description 4",
83+
technologies: ["Angular", "MySQL"]
84+
},
85+
{
86+
title: "Test Project 5",
87+
description: "Test description 5",
88+
technologies: ["Svelte", "Redis"]
89+
}
90+
];
91+
92+
it("shows only first 3 projects initially when more than 3 projects exist", () => {
93+
render(<FeaturedProjects projects={manyProjects} />);
94+
95+
const projectCards = screen.getAllByTestId("project-card");
96+
expect(projectCards).toHaveLength(3);
97+
98+
// Check that only first 3 projects are visible
99+
expect(screen.getByText("Test Project 1")).toBeInTheDocument();
100+
expect(screen.getByText("Test Project 2")).toBeInTheDocument();
101+
expect(screen.getByText("Test Project 3")).toBeInTheDocument();
102+
expect(screen.queryByText("Test Project 4")).not.toBeInTheDocument();
103+
expect(screen.queryByText("Test Project 5")).not.toBeInTheDocument();
104+
});
105+
106+
it("shows 'more...' button when more than 3 projects exist", () => {
107+
render(<FeaturedProjects projects={manyProjects} />);
108+
109+
const moreButton = screen.getByRole("button", { name: /more/i });
110+
expect(moreButton).toBeInTheDocument();
111+
expect(moreButton).toHaveTextContent("more...");
112+
});
113+
114+
it("does not show 'more...' button when 3 or fewer projects exist", () => {
115+
const threeProjects = manyProjects.slice(0, 3);
116+
render(<FeaturedProjects projects={threeProjects} />);
117+
118+
const moreButton = screen.queryByRole("button", { name: /more/i });
119+
expect(moreButton).not.toBeInTheDocument();
120+
});
121+
122+
it("shows all projects when 'more...' button is clicked", () => {
123+
render(<FeaturedProjects projects={manyProjects} />);
124+
125+
const moreButton = screen.getByRole("button", { name: /more/i });
126+
fireEvent.click(moreButton);
127+
128+
const projectCards = screen.getAllByTestId("project-card");
129+
expect(projectCards).toHaveLength(5);
130+
131+
// Check that all projects are now visible
132+
expect(screen.getByText("Test Project 1")).toBeInTheDocument();
133+
expect(screen.getByText("Test Project 2")).toBeInTheDocument();
134+
expect(screen.getByText("Test Project 3")).toBeInTheDocument();
135+
expect(screen.getByText("Test Project 4")).toBeInTheDocument();
136+
expect(screen.getByText("Test Project 5")).toBeInTheDocument();
137+
});
138+
139+
it("hides 'more...' button after it is clicked", () => {
140+
render(<FeaturedProjects projects={manyProjects} />);
141+
142+
const moreButton = screen.getByRole("button", { name: /more/i });
143+
fireEvent.click(moreButton);
144+
145+
expect(screen.queryByRole("button", { name: /more/i })).not.toBeInTheDocument();
146+
});
147+
148+
it("shows additional projects with animations when 'more...' button is clicked", () => {
149+
render(<FeaturedProjects projects={manyProjects} />);
150+
151+
const moreButton = screen.getByRole("button", { name: /more/i });
152+
fireEvent.click(moreButton);
153+
154+
// Additional projects should be rendered with motion.div wrapper
155+
const projectCards = screen.getAllByTestId("project-card");
156+
expect(projectCards).toHaveLength(5);
157+
158+
// The additional projects (4th and 5th) should be wrapped in motion.div
159+
const motionDivs = document.querySelectorAll('[data-testid="project-card"]');
160+
expect(motionDivs.length).toBe(5);
161+
});
162+
163+
it("positions 'more...' button in bottom right corner", () => {
164+
render(<FeaturedProjects projects={manyProjects} />);
165+
166+
const moreButton = screen.getByRole("button", { name: /more/i });
167+
const buttonContainer = moreButton.closest('[data-testid="more-button-container"]');
168+
169+
expect(buttonContainer).toBeInTheDocument();
170+
expect(buttonContainer).toHaveClass("flex", "justify-end", "mt-8");
171+
});
172+
173+
it("uses link variant for 'more...' button", () => {
174+
render(<FeaturedProjects projects={manyProjects} />);
175+
176+
const moreButton = screen.getByRole("button", { name: /more/i });
177+
expect(moreButton).toHaveAttribute("data-testid", "more-projects-button");
178+
// Since Button is mocked, we can't test the actual classes
179+
// The variant="link" prop is passed to the Button component
180+
});
181+
});
182+
63183
describe("Dynamic Grid Layout", () => {
64184
it("uses 2 columns layout for less than 3 projects", () => {
65185
const oneProject = [mockProjects[0]];

0 commit comments

Comments
 (0)