Skip to content

Commit d44637d

Browse files
committed
feat: add "more..." button to Technical Expertise section
- Show first 4 categories initially when more than 4 exist - Reveal remaining categories with animations on button click - Position button in bottom right corner with link styling - Maintain responsive grid layout and existing animations
1 parent e4946bd commit d44637d

2 files changed

Lines changed: 274 additions & 173 deletions

File tree

src/components/sections/TechnicalExpertise.tsx

Lines changed: 187 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { motion } from "framer-motion";
4+
import { useState } from "react";
45
import {
56
categoryStaggerVariants,
67
categorySlideInVariants,
@@ -10,13 +11,19 @@ import {
1011
sectionVariants
1112
} from "@/lib";
1213
import { type SkillCategory } from "@/content";
13-
import { Icon, OthersIconList } from "@/components/micro";
14+
import { Icon, OthersIconList, Button } from "@/components/micro";
1415

1516
export interface TechnicalExpertiseProps {
1617
categories?: SkillCategory[];
1718
}
1819

1920
export function TechnicalExpertise({ categories }: TechnicalExpertiseProps) {
21+
const [showAllCategories, setShowAllCategories] = useState(false);
22+
const categoryCount = categories?.length || 0;
23+
24+
const initialCategories = categories?.slice(0, 4) || [];
25+
const shouldShowMoreButton = categoryCount > 4;
26+
2027
return (
2128
<motion.section
2229
aria-label="Technical Expertise"
@@ -34,84 +41,186 @@ export function TechnicalExpertise({ categories }: TechnicalExpertiseProps) {
3441
<h2 className="font-mono text-[2rem] md:text-[2.5rem] font-semibold text-[#24292f] dark:text-[#f0f6fc]">
3542
Technical Expertise
3643
</h2>
37-
<motion.div
38-
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
39-
data-testid="categories-grid"
40-
variants={categoryStaggerVariants}
41-
>
42-
{categories?.map((category, index) => (
43-
<motion.div
44-
key={index}
45-
data-testid="category-card"
46-
className="bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
47-
variants={categorySlideInVariants}
48-
whileHover="hover"
49-
>
50-
<h3 className="font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]">
51-
{category.title}
52-
</h3>
53-
{category.skills && category.skills.length > 0 && (
54-
<motion.div
55-
className="mt-4 space-y-3"
56-
data-testid="skills-list"
57-
variants={staggerVariants}
58-
>
59-
{category.skills.map((skill, skillIndex) => (
60-
<motion.div
61-
key={skillIndex}
62-
variants={fadeVariants}
63-
className="group relative flex items-start gap-3"
64-
data-testid="skill-item"
65-
>
66-
{skill.icon && (
67-
<Icon
68-
name={skill.icon}
69-
alt={`${skill.name} icon`}
70-
size={32}
71-
className="flex-shrink-0 mt-0.5"
72-
/>
73-
)}
74-
<div className="flex-1 min-w-0">
75-
<div
76-
className="font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
77-
data-testid="skill-name"
78-
title={skill.years ? `${skill.years} years of experience` : undefined}
79-
>
80-
{skill.name}
81-
{skill.years && (
82-
<span className="ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200">
83-
({skill.years} years)
84-
</span>
85-
)}
44+
45+
{!showAllCategories ? (
46+
<motion.div
47+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
48+
data-testid="categories-grid"
49+
variants={categoryStaggerVariants}
50+
>
51+
{initialCategories?.map((category, index) => (
52+
<motion.div
53+
key={index}
54+
data-testid="category-card"
55+
className="bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
56+
variants={categorySlideInVariants}
57+
whileHover="hover"
58+
>
59+
<h3 className="font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]">
60+
{category.title}
61+
</h3>
62+
{category.skills && category.skills.length > 0 && (
63+
<motion.div
64+
className="mt-4 space-y-3"
65+
data-testid="skills-list"
66+
variants={staggerVariants}
67+
>
68+
{category.skills.map((skill, skillIndex) => (
69+
<motion.div
70+
key={skillIndex}
71+
variants={fadeVariants}
72+
className="group relative flex items-start gap-3"
73+
data-testid="skill-item"
74+
>
75+
{skill.icon && (
76+
<Icon
77+
name={skill.icon}
78+
alt={`${skill.name} icon`}
79+
size={32}
80+
className="flex-shrink-0 mt-0.5"
81+
/>
82+
)}
83+
<div className="flex-1 min-w-0">
84+
<div
85+
className="font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
86+
data-testid="skill-name"
87+
title={skill.years ? `${skill.years} years of experience` : undefined}
88+
>
89+
{skill.name}
90+
{skill.years && (
91+
<span className="ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200">
92+
({skill.years} years)
93+
</span>
94+
)}
95+
</div>
96+
<motion.div
97+
className="mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
98+
data-testid="proficiency-bar"
99+
variants={scaleVariants}
100+
>
101+
<motion.div
102+
className="absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
103+
data-testid="proficiency-fill"
104+
data-skill-name={skill.name}
105+
initial={{ width: "0%" }}
106+
whileInView={{ width: `${skill.proficiency}%` }}
107+
viewport={{ once: true }}
108+
transition={{
109+
duration: 1.2,
110+
ease: "easeOut",
111+
delay: 0.8
112+
}}
113+
/>
114+
</motion.div>
115+
</div>
116+
</motion.div>
117+
))}
118+
</motion.div>
119+
)}
120+
<OthersIconList others={category.others} />
121+
</motion.div>
122+
))}
123+
</motion.div>
124+
) : (
125+
<motion.div
126+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8 mt-12"
127+
data-testid="categories-grid"
128+
variants={categoryStaggerVariants}
129+
initial="hidden"
130+
animate="visible"
131+
>
132+
{categories?.map((category, index) => (
133+
<motion.div
134+
key={index}
135+
data-testid="category-card"
136+
className="bg-[#f6f8fa] dark:bg-[#21262d] border border-muted/20 rounded-lg p-6 hover:shadow-lg transition-all duration-300"
137+
variants={categorySlideInVariants}
138+
whileHover="hover"
139+
>
140+
<h3 className="font-mono text-xl font-semibold text-[#24292f] dark:text-[#f0f6fc]">
141+
{category.title}
142+
</h3>
143+
{category.skills && category.skills.length > 0 && (
144+
<motion.div
145+
className="mt-4 space-y-3"
146+
data-testid="skills-list"
147+
variants={staggerVariants}
148+
>
149+
{category.skills.map((skill, skillIndex) => (
150+
<motion.div
151+
key={skillIndex}
152+
variants={fadeVariants}
153+
className="group relative flex items-start gap-3"
154+
data-testid="skill-item"
155+
>
156+
{skill.icon && (
157+
<Icon
158+
name={skill.icon}
159+
alt={`${skill.name} icon`}
160+
size={32}
161+
className="flex-shrink-0 mt-0.5"
162+
/>
163+
)}
164+
<div className="flex-1 min-w-0">
165+
<div
166+
className="font-sans text-sm font-medium text-[#24292f] dark:text-[#f0f6fc] cursor-pointer"
167+
data-testid="skill-name"
168+
title={skill.years ? `${skill.years} years of experience` : undefined}
169+
>
170+
{skill.name}
171+
{skill.years && (
172+
<span className="ml-2 text-xs text-[#656d76] dark:text-[#8b949e] opacity-0 group-hover:opacity-100 transition-opacity duration-200">
173+
({skill.years} years)
174+
</span>
175+
)}
176+
</div>
177+
<motion.div
178+
className="mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
179+
data-testid="proficiency-bar"
180+
variants={scaleVariants}
181+
>
182+
<motion.div
183+
className="absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
184+
data-testid="proficiency-fill"
185+
data-skill-name={skill.name}
186+
initial={{ width: "0%" }}
187+
whileInView={{ width: `${skill.proficiency}%` }}
188+
viewport={{ once: true }}
189+
transition={{
190+
duration: 1.2,
191+
ease: "easeOut",
192+
delay: 0.8
193+
}}
194+
/>
195+
</motion.div>
86196
</div>
87-
<motion.div
88-
className="mt-1 relative h-2 bg-[#e1e4e8] dark:bg-[#30363d] rounded-full"
89-
data-testid="proficiency-bar"
90-
variants={scaleVariants}
91-
>
92-
<motion.div
93-
className="absolute top-0 left-0 h-full bg-[#0969da] dark:bg-[#58a6ff] rounded-full"
94-
data-testid="proficiency-fill"
95-
data-skill-name={skill.name}
96-
initial={{ width: "0%" }}
97-
whileInView={{ width: `${skill.proficiency}%` }}
98-
viewport={{ once: true }}
99-
transition={{
100-
duration: 1.2,
101-
ease: "easeOut",
102-
delay: 0.8
103-
}}
104-
/>
105-
</motion.div>
106-
</div>
107-
</motion.div>
108-
))}
109-
</motion.div>
110-
)}
111-
<OthersIconList others={category.others} />
112-
</motion.div>
113-
))}
114-
</motion.div>
197+
</motion.div>
198+
))}
199+
</motion.div>
200+
)}
201+
<OthersIconList others={category.others} />
202+
</motion.div>
203+
))}
204+
</motion.div>
205+
)}
206+
207+
{shouldShowMoreButton && !showAllCategories && (
208+
<motion.div
209+
className="flex justify-end mt-8"
210+
data-testid="more-button-container"
211+
initial={{ opacity: 0, y: 20 }}
212+
animate={{ opacity: 1, y: 0 }}
213+
transition={{ duration: 0.5, delay: 0.5 }}
214+
>
215+
<Button
216+
variant="link"
217+
onClick={() => setShowAllCategories(true)}
218+
data-testid="more-categories-button"
219+
>
220+
more...
221+
</Button>
222+
</motion.div>
223+
)}
115224
</div>
116225
</motion.section>
117226
);

0 commit comments

Comments
 (0)